mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-23 22:28:22 -05:00
Refactor pin setup in Android to use native view (#1340)
This commit is contained in:
committed by
Leendert de Borst
parent
4b59776b86
commit
558d39ec96
@@ -103,7 +103,7 @@ class MainActivity : ReactActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity results - specifically for PIN unlock.
|
||||
* Handle activity results - specifically for PIN unlock and PIN setup.
|
||||
*/
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -111,6 +111,8 @@ class MainActivity : ReactActivity() {
|
||||
// Handle PIN unlock results directly
|
||||
if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_UNLOCK_REQUEST_CODE) {
|
||||
handlePinUnlockResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_SETUP_REQUEST_CODE) {
|
||||
handlePinSetupResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,4 +165,33 @@ class MainActivity : ReactActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle PIN setup result.
|
||||
* @param resultCode The result code from the PIN setup activity.
|
||||
* @param data The intent data (not used for setup, setup happens internally).
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handlePinSetupResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pinSetupPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.pinSetupPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when (resultCode) {
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_SUCCESS -> {
|
||||
// PIN setup successful
|
||||
promise.resolve(null)
|
||||
}
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_CANCELLED -> {
|
||||
// User cancelled PIN setup
|
||||
promise.reject("USER_CANCELLED", "User cancelled PIN setup", null)
|
||||
}
|
||||
else -> {
|
||||
promise.reject("SETUP_ERROR", "PIN setup failed", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
*/
|
||||
const val PIN_UNLOCK_REQUEST_CODE = 1001
|
||||
|
||||
/**
|
||||
* Request code for PIN setup activity.
|
||||
*/
|
||||
const val PIN_SETUP_REQUEST_CODE = 1002
|
||||
|
||||
/**
|
||||
* Static holder for the pending promise from showPinUnlockUI.
|
||||
* This allows MainActivity to resolve/reject the promise directly without
|
||||
@@ -64,6 +69,14 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
*/
|
||||
@Volatile
|
||||
var pendingActivityResultPromise: Promise? = null
|
||||
|
||||
/**
|
||||
* Static holder for the pending promise from showNativePinSetup.
|
||||
* This allows MainActivity to resolve/reject the promise directly without
|
||||
* depending on React context availability.
|
||||
*/
|
||||
@Volatile
|
||||
var pinSetupPromise: Promise? = null
|
||||
}
|
||||
|
||||
private val vaultStore = VaultStore.getInstance(
|
||||
@@ -1315,6 +1328,46 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show native PIN setup UI.
|
||||
* Launches the native PinUnlockActivity in setup mode.
|
||||
* Gets the vault encryption key from memory (vault must be unlocked).
|
||||
* @param promise The promise to resolve when setup completes or rejects if cancelled/error.
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun showNativePinSetup(promise: Promise) {
|
||||
// Get encryption key first
|
||||
vaultStore.getEncryptionKey(object : net.aliasvault.app.vaultstore.interfaces.CryptoOperationCallback {
|
||||
override fun onSuccess(encryptionKey: String) {
|
||||
try {
|
||||
val activity = currentActivity
|
||||
if (activity == null) {
|
||||
promise.reject("ERR_NO_ACTIVITY", "No activity available")
|
||||
return
|
||||
}
|
||||
|
||||
// Store the promise for later resolution
|
||||
pinSetupPromise = promise
|
||||
|
||||
// Launch PIN setup activity
|
||||
val intent = android.content.Intent(activity, net.aliasvault.app.pinunlock.PinUnlockActivity::class.java)
|
||||
intent.putExtra(net.aliasvault.app.pinunlock.PinUnlockActivity.EXTRA_MODE, net.aliasvault.app.pinunlock.PinUnlockActivity.MODE_SETUP)
|
||||
intent.putExtra(net.aliasvault.app.pinunlock.PinUnlockActivity.EXTRA_SETUP_ENCRYPTION_KEY, encryptionKey)
|
||||
|
||||
activity.startActivityForResult(intent, PIN_SETUP_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error launching PIN setup activity", e)
|
||||
promise.reject("ERR_LAUNCH_PIN_SETUP", "Failed to launch PIN setup: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: Exception) {
|
||||
Log.e(TAG, "Error getting encryption key for PIN setup", error)
|
||||
promise.reject("ERR_SETUP_PIN", "Failed to get encryption key: ${error.message}", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock with PIN.
|
||||
* @param pin The PIN to unlock with.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package net.aliasvault.app.pinunlock
|
||||
|
||||
/**
|
||||
* Represents the configuration and state for PIN entry screen.
|
||||
*/
|
||||
data class PinConfiguration(
|
||||
/** The mode of operation: unlock or setup. */
|
||||
val mode: PinMode,
|
||||
/** The title to display. */
|
||||
val title: String,
|
||||
/** The subtitle/description to display. */
|
||||
val subtitle: String,
|
||||
/** The expected PIN length (null for variable length). */
|
||||
val pinLength: Int?,
|
||||
/** The current step in setup mode. */
|
||||
val setupStep: PinSetupStep = PinSetupStep.ENTER_NEW,
|
||||
/** The PIN entered in the first step (setup mode only). */
|
||||
val firstStepPin: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Modes of PIN operation.
|
||||
*/
|
||||
enum class PinMode {
|
||||
/** Unlock vault with existing PIN. */
|
||||
UNLOCK,
|
||||
|
||||
/** Setup a new PIN (two-step process). */
|
||||
SETUP,
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps in PIN setup process.
|
||||
*/
|
||||
enum class PinSetupStep {
|
||||
/** First step: enter new PIN. */
|
||||
ENTER_NEW,
|
||||
|
||||
/** Second step: confirm the PIN. */
|
||||
CONFIRM,
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of PIN processing.
|
||||
*/
|
||||
sealed class PinResult {
|
||||
/**
|
||||
* PIN processing succeeded.
|
||||
* @property encryptionKey The encryption key (returned for unlock mode, null for setup mode).
|
||||
*/
|
||||
data class Success(val encryptionKey: String?) : PinResult()
|
||||
|
||||
/**
|
||||
* PIN processing failed with an error.
|
||||
* @property message The error message to display.
|
||||
* @property shouldClear Whether to clear the entered PIN.
|
||||
*/
|
||||
data class Error(val message: String, val shouldClear: Boolean = true) : PinResult()
|
||||
|
||||
/** PIN was disabled due to max attempts. */
|
||||
object PinDisabled : PinResult()
|
||||
|
||||
/**
|
||||
* Move to next step in setup flow.
|
||||
* @property newConfiguration The configuration for the next step.
|
||||
*/
|
||||
data class NextStep(val newConfiguration: PinConfiguration) : PinResult()
|
||||
|
||||
/**
|
||||
* PINs don't match in setup confirmation.
|
||||
* @property errorMessage The error message to display.
|
||||
*/
|
||||
data class Mismatch(val errorMessage: String) : PinResult()
|
||||
}
|
||||
@@ -18,40 +18,77 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.aliasvault.app.R
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
|
||||
/**
|
||||
* Native PIN unlock activity matching iOS PinUnlockView.
|
||||
* This activity presents a numpad UI for PIN entry and handles vault unlocking.
|
||||
* Native PIN unlock and setup activity.
|
||||
* This activity presents a numpad UI for PIN entry and handles both vault unlocking and PIN setup.
|
||||
*
|
||||
* Modes:
|
||||
* - UNLOCK: Unlock vault with existing PIN
|
||||
* - SETUP: Setup new PIN (two-step: enter → confirm)
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* // Unlock mode
|
||||
* val intent = Intent(context, PinUnlockActivity::class.java)
|
||||
* intent.putExtra(PinUnlockActivity.EXTRA_MODE, PinUnlockActivity.MODE_UNLOCK)
|
||||
* startActivityForResult(intent, REQUEST_CODE)
|
||||
*
|
||||
* // Setup mode
|
||||
* val intent = Intent(context, PinUnlockActivity::class.java)
|
||||
* intent.putExtra(PinUnlockActivity.EXTRA_MODE, PinUnlockActivity.MODE_SETUP)
|
||||
* intent.putExtra(PinUnlockActivity.EXTRA_SETUP_ENCRYPTION_KEY, encryptionKey)
|
||||
* startActivityForResult(intent, REQUEST_CODE)
|
||||
* ```
|
||||
*/
|
||||
class PinUnlockActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
/** Result code for successful PIN unlock. */
|
||||
/** Result code for successful PIN unlock or setup. */
|
||||
const val RESULT_SUCCESS = Activity.RESULT_OK
|
||||
|
||||
/** Result code for cancelled PIN unlock. */
|
||||
/** Result code for cancelled PIN unlock or setup. */
|
||||
const val RESULT_CANCELLED = Activity.RESULT_CANCELED
|
||||
|
||||
/** Result code when PIN was disabled due to max attempts. */
|
||||
const val RESULT_PIN_DISABLED = 100
|
||||
|
||||
/** Intent extra key for the encryption key. */
|
||||
/** Intent extra key for the encryption key (returned in unlock mode). */
|
||||
const val EXTRA_ENCRYPTION_KEY = "encryption_key"
|
||||
|
||||
/** Intent extra key for the mode (unlock or setup). */
|
||||
const val EXTRA_MODE = "mode"
|
||||
|
||||
/** Intent extra key for the encryption key to use during setup. */
|
||||
const val EXTRA_SETUP_ENCRYPTION_KEY = "setup_encryption_key"
|
||||
|
||||
/** Mode: Unlock vault with existing PIN. */
|
||||
const val MODE_UNLOCK = "unlock"
|
||||
|
||||
/** Mode: Setup new PIN. */
|
||||
const val MODE_SETUP = "setup"
|
||||
}
|
||||
|
||||
private lateinit var viewModel: PinViewModel
|
||||
private lateinit var vaultStore: VaultStore
|
||||
|
||||
// UI components
|
||||
private lateinit var titleTextView: TextView
|
||||
private lateinit var subtitleTextView: TextView
|
||||
private lateinit var pinDotsContainer: LinearLayout
|
||||
private lateinit var pinTextView: TextView
|
||||
private lateinit var errorTextView: TextView
|
||||
private lateinit var continueButton: Button
|
||||
private lateinit var loadingOverlay: View
|
||||
private lateinit var progressBar: ProgressBar
|
||||
|
||||
private var pinLength: Int? = null
|
||||
// State
|
||||
private var configuration: PinConfiguration? = null
|
||||
private var setupEncryptionKey: String? = null
|
||||
private var currentPin: String = ""
|
||||
private var isUnlocking: Boolean = false
|
||||
private var isProcessing: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -60,12 +97,37 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
// Keep screen on during PIN entry
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
// Apply window insets to the root layout for safe area
|
||||
findViewById<android.view.View>(android.R.id.content).setOnApplyWindowInsetsListener { view, insets ->
|
||||
// Apply window insets for safe area
|
||||
applyWindowInsets()
|
||||
|
||||
// Initialize VaultStore and ViewModel
|
||||
vaultStore = VaultStore.getInstance(
|
||||
net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider(this) { null },
|
||||
net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider(this),
|
||||
)
|
||||
viewModel = PinViewModel(this, vaultStore)
|
||||
|
||||
// Get mode and encryption key from intent
|
||||
val mode = when (intent.getStringExtra(EXTRA_MODE)) {
|
||||
MODE_SETUP -> PinMode.SETUP
|
||||
else -> PinMode.UNLOCK
|
||||
}
|
||||
setupEncryptionKey = intent.getStringExtra(EXTRA_SETUP_ENCRYPTION_KEY)
|
||||
|
||||
// Initialize configuration
|
||||
configuration = viewModel.initializeConfiguration(mode)
|
||||
|
||||
// Initialize views
|
||||
initializeViews()
|
||||
setupNumpad()
|
||||
updateUI()
|
||||
}
|
||||
|
||||
private fun applyWindowInsets() {
|
||||
findViewById<View>(android.R.id.content).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val cancelButton = findViewById<Button>(R.id.cancelButton)
|
||||
val systemBarsInsets = insets.systemWindowInsets
|
||||
|
||||
// Apply top inset to cancel button
|
||||
cancelButton.setPadding(
|
||||
cancelButton.paddingLeft,
|
||||
systemBarsInsets.top + cancelButton.paddingTop,
|
||||
@@ -75,25 +137,15 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
|
||||
insets
|
||||
}
|
||||
|
||||
// Initialize VaultStore
|
||||
vaultStore = VaultStore.getInstance(
|
||||
net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider(this) { null },
|
||||
net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider(this),
|
||||
)
|
||||
|
||||
// Get PIN length
|
||||
pinLength = vaultStore.getPinLength()
|
||||
|
||||
// Initialize views
|
||||
setupViews()
|
||||
setupNumpad()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
private fun initializeViews() {
|
||||
titleTextView = findViewById(R.id.titleTextView)
|
||||
subtitleTextView = findViewById(R.id.subtitleTextView)
|
||||
pinDotsContainer = findViewById(R.id.pinDotsContainer)
|
||||
pinTextView = findViewById(R.id.pinTextView)
|
||||
errorTextView = findViewById(R.id.errorTextView)
|
||||
continueButton = findViewById(R.id.continueButton)
|
||||
loadingOverlay = findViewById(R.id.loadingOverlay)
|
||||
progressBar = findViewById(R.id.progressBar)
|
||||
|
||||
@@ -103,18 +155,62 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Continue button (for PIN setup)
|
||||
continueButton.setOnClickListener {
|
||||
submitPin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNumpad() {
|
||||
// Number buttons
|
||||
findViewById<Button>(R.id.btn1).setOnClickListener { addDigit("1") }
|
||||
findViewById<Button>(R.id.btn2).setOnClickListener { addDigit("2") }
|
||||
findViewById<Button>(R.id.btn3).setOnClickListener { addDigit("3") }
|
||||
findViewById<Button>(R.id.btn4).setOnClickListener { addDigit("4") }
|
||||
findViewById<Button>(R.id.btn5).setOnClickListener { addDigit("5") }
|
||||
findViewById<Button>(R.id.btn6).setOnClickListener { addDigit("6") }
|
||||
findViewById<Button>(R.id.btn7).setOnClickListener { addDigit("7") }
|
||||
findViewById<Button>(R.id.btn8).setOnClickListener { addDigit("8") }
|
||||
findViewById<Button>(R.id.btn9).setOnClickListener { addDigit("9") }
|
||||
findViewById<Button>(R.id.btn0).setOnClickListener { addDigit("0") }
|
||||
|
||||
// Backspace button
|
||||
findViewById<Button>(R.id.btnBackspace).setOnClickListener { removeDigit() }
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
val config = configuration ?: return
|
||||
|
||||
// Update title and subtitle
|
||||
titleTextView.text = config.title
|
||||
subtitleTextView.text = config.subtitle
|
||||
|
||||
// Show Continue button only for setup mode on first step (variable length)
|
||||
val showContinueButton = config.mode == PinMode.SETUP &&
|
||||
config.setupStep == PinSetupStep.ENTER_NEW &&
|
||||
config.pinLength == null
|
||||
continueButton.visibility = if (showContinueButton) View.VISIBLE else View.GONE
|
||||
|
||||
// Update button text based on current step
|
||||
if (showContinueButton) {
|
||||
continueButton.text = getString(R.string.pin_next)
|
||||
}
|
||||
|
||||
// Setup PIN display based on whether we have a fixed length
|
||||
if (pinLength != null) {
|
||||
if (config.pinLength != null) {
|
||||
// Show dots for fixed length PIN
|
||||
pinDotsContainer.visibility = View.VISIBLE
|
||||
pinTextView.visibility = View.GONE
|
||||
createPinDots(pinLength!!)
|
||||
createPinDots(config.pinLength)
|
||||
} else {
|
||||
// Show text for variable length PIN
|
||||
pinDotsContainer.visibility = View.GONE
|
||||
pinTextView.visibility = View.VISIBLE
|
||||
updatePinText()
|
||||
}
|
||||
|
||||
// Update continue button enabled state
|
||||
updateContinueButtonState()
|
||||
}
|
||||
|
||||
private fun createPinDots(count: Int) {
|
||||
@@ -154,58 +250,51 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNumpad() {
|
||||
// Number buttons
|
||||
findViewById<Button>(R.id.btn1).setOnClickListener { addDigit("1") }
|
||||
findViewById<Button>(R.id.btn2).setOnClickListener { addDigit("2") }
|
||||
findViewById<Button>(R.id.btn3).setOnClickListener { addDigit("3") }
|
||||
findViewById<Button>(R.id.btn4).setOnClickListener { addDigit("4") }
|
||||
findViewById<Button>(R.id.btn5).setOnClickListener { addDigit("5") }
|
||||
findViewById<Button>(R.id.btn6).setOnClickListener { addDigit("6") }
|
||||
findViewById<Button>(R.id.btn7).setOnClickListener { addDigit("7") }
|
||||
findViewById<Button>(R.id.btn8).setOnClickListener { addDigit("8") }
|
||||
findViewById<Button>(R.id.btn9).setOnClickListener { addDigit("9") }
|
||||
findViewById<Button>(R.id.btn0).setOnClickListener { addDigit("0") }
|
||||
|
||||
// Backspace button
|
||||
findViewById<Button>(R.id.btnBackspace).setOnClickListener { removeDigit() }
|
||||
private fun updateContinueButtonState() {
|
||||
val config = configuration ?: return
|
||||
if (continueButton.visibility == View.VISIBLE) {
|
||||
// Enable button only if PIN is at least 4 digits
|
||||
continueButton.isEnabled = currentPin.length >= 4
|
||||
continueButton.alpha = if (currentPin.length >= 4) 1.0f else 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDigit(digit: String) {
|
||||
if (isUnlocking) return
|
||||
if (isProcessing) return
|
||||
|
||||
val config = configuration ?: return
|
||||
|
||||
// Clear error when user starts typing
|
||||
errorTextView.visibility = View.GONE
|
||||
|
||||
// Check if we've reached max length
|
||||
pinLength?.let { maxLength ->
|
||||
if (currentPin.length >= maxLength) return
|
||||
}
|
||||
val maxLength = config.pinLength ?: 8 // Max 8 digits in setup mode
|
||||
if (currentPin.length >= maxLength) return
|
||||
|
||||
// Add digit
|
||||
currentPin += digit
|
||||
|
||||
// Update UI
|
||||
if (pinLength != null) {
|
||||
if (config.pinLength != null) {
|
||||
updatePinDots()
|
||||
} else {
|
||||
updatePinText()
|
||||
}
|
||||
|
||||
// Auto-submit when PIN reaches expected length
|
||||
pinLength?.let { expectedLength ->
|
||||
if (currentPin.length == expectedLength) {
|
||||
// Small delay to show the last dot filled before attempting unlock
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(100)
|
||||
attemptUnlock()
|
||||
}
|
||||
// Update continue button state
|
||||
updateContinueButtonState()
|
||||
|
||||
// Auto-submit when PIN reaches expected length (only for fixed-length PINs)
|
||||
if (viewModel.shouldAutoSubmit(currentPin.length, config)) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(100) // Small delay to show the last dot filled
|
||||
submitPin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDigit() {
|
||||
if (isUnlocking || currentPin.isEmpty()) return
|
||||
if (isProcessing || currentPin.isEmpty()) return
|
||||
|
||||
// Remove last digit
|
||||
currentPin = currentPin.dropLast(1)
|
||||
@@ -214,77 +303,111 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
errorTextView.visibility = View.GONE
|
||||
|
||||
// Update UI
|
||||
if (pinLength != null) {
|
||||
val config = configuration ?: return
|
||||
if (config.pinLength != null) {
|
||||
updatePinDots()
|
||||
} else {
|
||||
updatePinText()
|
||||
}
|
||||
|
||||
// Update continue button state
|
||||
updateContinueButtonState()
|
||||
}
|
||||
|
||||
private fun attemptUnlock() {
|
||||
if (isUnlocking) return
|
||||
private fun submitPin() {
|
||||
if (isProcessing || currentPin.isEmpty()) return
|
||||
|
||||
val config = configuration ?: return
|
||||
|
||||
// Validate minimum length for setup mode
|
||||
if (config.mode == PinMode.SETUP &&
|
||||
config.setupStep == PinSetupStep.ENTER_NEW &&
|
||||
currentPin.length < 4
|
||||
) {
|
||||
return // Don't submit until at least 4 digits
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
// Show loading state
|
||||
isUnlocking = true
|
||||
isProcessing = true
|
||||
loadingOverlay.visibility = View.VISIBLE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
// Give UI time to update
|
||||
delay(50)
|
||||
|
||||
// Perform unlock in background (Argon2 is CPU intensive)
|
||||
val encryptionKeyBase64 = withContext(Dispatchers.IO) {
|
||||
vaultStore.unlockWithPin(currentPin)
|
||||
}
|
||||
// Process the PIN
|
||||
val result = viewModel.processPin(currentPin, config, setupEncryptionKey)
|
||||
|
||||
// Success - return encryption key
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(EXTRA_ENCRYPTION_KEY, encryptionKeyBase64)
|
||||
// Handle result
|
||||
handlePinResult(result)
|
||||
} catch (e: Exception) {
|
||||
isProcessing = false
|
||||
loadingOverlay.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
showError(e.message ?: "An error occurred")
|
||||
triggerErrorFeedback()
|
||||
shakeAndClear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePinResult(result: PinResult) {
|
||||
when (result) {
|
||||
is PinResult.Success -> {
|
||||
// Success - return result
|
||||
val resultIntent = Intent()
|
||||
result.encryptionKey?.let {
|
||||
resultIntent.putExtra(EXTRA_ENCRYPTION_KEY, it)
|
||||
}
|
||||
setResult(RESULT_SUCCESS, resultIntent)
|
||||
finish()
|
||||
} catch (e: net.aliasvault.app.vaultstore.PinUnlockException) {
|
||||
// Handle PinUnlockException with localized strings
|
||||
isUnlocking = false
|
||||
}
|
||||
is PinResult.Error -> {
|
||||
isProcessing = false
|
||||
loadingOverlay.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
when (e) {
|
||||
is net.aliasvault.app.vaultstore.PinUnlockException.NotConfigured -> {
|
||||
// PIN not configured - show error briefly then dismiss
|
||||
showError(getString(R.string.pin_not_configured))
|
||||
triggerErrorFeedback()
|
||||
delay(1000)
|
||||
setResult(RESULT_PIN_DISABLED)
|
||||
finish()
|
||||
}
|
||||
is net.aliasvault.app.vaultstore.PinUnlockException.Locked -> {
|
||||
// PIN locked after max attempts - show error briefly then dismiss
|
||||
showError(getString(R.string.pin_locked_max_attempts))
|
||||
triggerErrorFeedback()
|
||||
delay(1000)
|
||||
setResult(RESULT_PIN_DISABLED)
|
||||
finish()
|
||||
}
|
||||
is net.aliasvault.app.vaultstore.PinUnlockException.IncorrectPin -> {
|
||||
// Incorrect PIN - show error with attempts remaining and clear
|
||||
val errorMessage = getString(R.string.pin_incorrect_attempts_remaining, e.attemptsRemaining)
|
||||
showError(errorMessage)
|
||||
triggerErrorFeedback()
|
||||
shakeAndClear()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Fallback for any other errors
|
||||
isUnlocking = false
|
||||
loadingOverlay.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
showError(e.message ?: getString(R.string.pin_unlock_failed))
|
||||
showError(result.message)
|
||||
triggerErrorFeedback()
|
||||
shakeAndClear()
|
||||
if (result.shouldClear) {
|
||||
shakeAndClear()
|
||||
}
|
||||
}
|
||||
is PinResult.PinDisabled -> {
|
||||
showError(getString(R.string.pin_locked_max_attempts))
|
||||
triggerErrorFeedback()
|
||||
delay(1000)
|
||||
setResult(RESULT_PIN_DISABLED)
|
||||
finish()
|
||||
}
|
||||
is PinResult.NextStep -> {
|
||||
// Move to next step (setup confirmation)
|
||||
isProcessing = false
|
||||
loadingOverlay.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
configuration = result.newConfiguration
|
||||
currentPin = ""
|
||||
updateUI()
|
||||
}
|
||||
is PinResult.Mismatch -> {
|
||||
// PINs don't match - restart setup
|
||||
isProcessing = false
|
||||
loadingOverlay.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
showError(result.errorMessage)
|
||||
triggerErrorFeedback()
|
||||
|
||||
// Restart from beginning after showing error
|
||||
delay(1000)
|
||||
configuration = viewModel.initializeConfiguration(PinMode.SETUP)
|
||||
currentPin = ""
|
||||
errorTextView.visibility = View.GONE
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +433,8 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
// Clear the PIN after a short delay to show error
|
||||
delay(500)
|
||||
currentPin = ""
|
||||
if (pinLength != null) {
|
||||
val config = configuration ?: return@launch
|
||||
if (config.pinLength != null) {
|
||||
updatePinDots()
|
||||
} else {
|
||||
updatePinText()
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package net.aliasvault.app.pinunlock
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.aliasvault.app.R
|
||||
import net.aliasvault.app.vaultstore.PinUnlockException
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
|
||||
/**
|
||||
* ViewModel for PIN unlock and setup operations.
|
||||
* Handles business logic and state management for PIN flows.
|
||||
*/
|
||||
class PinViewModel(
|
||||
private val context: Context,
|
||||
private val vaultStore: VaultStore,
|
||||
) {
|
||||
/**
|
||||
* Initialize PIN configuration based on mode.
|
||||
*/
|
||||
fun initializeConfiguration(mode: PinMode): PinConfiguration {
|
||||
return when (mode) {
|
||||
PinMode.UNLOCK -> {
|
||||
val pinLength = vaultStore.getPinLength()
|
||||
PinConfiguration(
|
||||
mode = PinMode.UNLOCK,
|
||||
title = context.getString(R.string.pin_unlock_vault),
|
||||
subtitle = context.getString(R.string.pin_enter_to_unlock),
|
||||
pinLength = pinLength,
|
||||
)
|
||||
}
|
||||
PinMode.SETUP -> {
|
||||
PinConfiguration(
|
||||
mode = PinMode.SETUP,
|
||||
title = context.getString(R.string.pin_setup_title),
|
||||
subtitle = context.getString(R.string.pin_setup_description),
|
||||
pinLength = null, // Allow any length from 4-8
|
||||
setupStep = PinSetupStep.ENTER_NEW,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the entered PIN based on current configuration.
|
||||
*/
|
||||
suspend fun processPin(
|
||||
pin: String,
|
||||
configuration: PinConfiguration,
|
||||
setupEncryptionKey: String? = null,
|
||||
): PinResult = withContext(Dispatchers.IO) {
|
||||
return@withContext when (configuration.mode) {
|
||||
PinMode.UNLOCK -> processUnlock(pin)
|
||||
PinMode.SETUP -> processSetup(pin, configuration, setupEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process unlock attempt.
|
||||
*/
|
||||
private fun processUnlock(pin: String): PinResult {
|
||||
return try {
|
||||
val encryptionKey = vaultStore.unlockWithPin(pin)
|
||||
PinResult.Success(encryptionKey)
|
||||
} catch (e: PinUnlockException) {
|
||||
when (e) {
|
||||
is PinUnlockException.NotConfigured -> {
|
||||
PinResult.Error(
|
||||
context.getString(R.string.pin_not_configured),
|
||||
shouldClear = false,
|
||||
)
|
||||
}
|
||||
is PinUnlockException.Locked -> {
|
||||
PinResult.PinDisabled
|
||||
}
|
||||
is PinUnlockException.IncorrectPin -> {
|
||||
PinResult.Error(
|
||||
context.getString(R.string.pin_incorrect_attempts_remaining, e.attemptsRemaining),
|
||||
shouldClear = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
PinResult.Error(
|
||||
e.message ?: context.getString(R.string.pin_unlock_failed),
|
||||
shouldClear = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process setup flow.
|
||||
*/
|
||||
private fun processSetup(
|
||||
pin: String,
|
||||
configuration: PinConfiguration,
|
||||
setupEncryptionKey: String?,
|
||||
): PinResult {
|
||||
return when (configuration.setupStep) {
|
||||
PinSetupStep.ENTER_NEW -> {
|
||||
// Validate PIN length
|
||||
if (pin.length < 4) {
|
||||
return PinResult.Error(
|
||||
"PIN must be at least 4 digits",
|
||||
shouldClear = false,
|
||||
)
|
||||
}
|
||||
if (pin.length > 8) {
|
||||
return PinResult.Error(
|
||||
"PIN must be at most 8 digits",
|
||||
shouldClear = false,
|
||||
)
|
||||
}
|
||||
|
||||
// Move to confirm step
|
||||
val newConfig = configuration.copy(
|
||||
title = context.getString(R.string.pin_confirm_title),
|
||||
subtitle = context.getString(R.string.pin_confirm_description),
|
||||
setupStep = PinSetupStep.CONFIRM,
|
||||
firstStepPin = pin,
|
||||
pinLength = pin.length, // Fix length for confirmation
|
||||
)
|
||||
PinResult.NextStep(newConfig)
|
||||
}
|
||||
PinSetupStep.CONFIRM -> {
|
||||
// Check if PINs match
|
||||
if (pin != configuration.firstStepPin) {
|
||||
return PinResult.Mismatch(context.getString(R.string.pin_mismatch))
|
||||
}
|
||||
|
||||
// Setup the PIN
|
||||
try {
|
||||
if (setupEncryptionKey == null) {
|
||||
return PinResult.Error(
|
||||
"Encryption key required for PIN setup",
|
||||
shouldClear = false,
|
||||
)
|
||||
}
|
||||
vaultStore.setupPin(pin, setupEncryptionKey)
|
||||
PinResult.Success(null)
|
||||
} catch (e: Exception) {
|
||||
PinResult.Error(
|
||||
e.message ?: "Failed to setup PIN",
|
||||
shouldClear = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PIN entry should auto-submit based on configuration.
|
||||
*/
|
||||
fun shouldAutoSubmit(pinLength: Int, configuration: PinConfiguration): Boolean {
|
||||
// Auto-submit when:
|
||||
// 1. In unlock mode with fixed length PIN
|
||||
// 2. In confirm step with matching length
|
||||
return when {
|
||||
configuration.mode == PinMode.UNLOCK && configuration.pinLength != null -> {
|
||||
pinLength == configuration.pinLength
|
||||
}
|
||||
configuration.mode == PinMode.SETUP &&
|
||||
configuration.setupStep == PinSetupStep.CONFIRM &&
|
||||
configuration.pinLength != null -> {
|
||||
pinLength == configuration.pinLength
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,24 @@
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginEnd="40dp" />
|
||||
|
||||
<!-- Continue Button (for PIN setup) -->
|
||||
<Button
|
||||
android:id="@+id/continueButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:text="@string/pin_next"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/numpadContainer"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginEnd="40dp" />
|
||||
|
||||
<!-- Numpad -->
|
||||
<LinearLayout
|
||||
android:id="@+id/numpadContainer"
|
||||
|
||||
@@ -80,4 +80,13 @@
|
||||
<string name="pin_not_configured">PIN unlock is not configured</string>
|
||||
<string name="pin_locked_max_attempts">PIN locked after too many failed attempts</string>
|
||||
<string name="pin_incorrect_attempts_remaining">Incorrect PIN. %d attempts remaining</string>
|
||||
|
||||
<!-- PIN setup -->
|
||||
<string name="pin_setup_title">Setup PIN</string>
|
||||
<string name="pin_setup_description">Choose a PIN to unlock your vault</string>
|
||||
<string name="pin_confirm_title">Confirm PIN</string>
|
||||
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
|
||||
<string name="pin_mismatch">PINs do not match. Please try again.</string>
|
||||
<string name="pin_next">Next</string>
|
||||
<string name="pin_confirm">Confirm</string>
|
||||
</resources>
|
||||
@@ -1,20 +1,18 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, Platform, Linking, Switch, TouchableOpacity, Modal } from 'react-native';
|
||||
import { StyleSheet, View, Alert, Platform, Linking, Switch, TouchableOpacity } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { isPinEnabled, setupPin, removeAndDisablePin } from '@/utils/PinUnlockService';
|
||||
import { isPinEnabled, removeAndDisablePin } from '@/utils/PinUnlockService';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { PinNumpad } from '@/components/pin/PinNumpad';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { AuthMethod, useAuth } from '@/context/AuthContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
/**
|
||||
* Vault unlock settings screen.
|
||||
@@ -31,11 +29,6 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
|
||||
// PIN state
|
||||
const [pinEnabled, setPinEnabled] = useState(false);
|
||||
const [showPinSetup, setShowPinSetup] = useState(false);
|
||||
const [pinSetupStep, setPinSetupStep] = useState(1);
|
||||
const [newPin, setNewPin] = useState('');
|
||||
const [confirmPin, setConfirmPin] = useState('');
|
||||
const [pinError, setPinError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -166,15 +159,41 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
}, [hasBiometrics, pinEnabled, setAuthMethods, biometricDisplayName, t]);
|
||||
|
||||
/**
|
||||
* Handle enable PIN.
|
||||
* Handle enable PIN - launches native PIN setup UI.
|
||||
*/
|
||||
const handleEnablePin = useCallback(() : void => {
|
||||
// Don't disable biometrics yet - only after successful PIN setup
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setShowPinSetup(true);
|
||||
}, []);
|
||||
const handleEnablePin = useCallback(async () : Promise<void> => {
|
||||
try {
|
||||
// Launch native PIN setup UI
|
||||
await NativeVaultManager.showNativePinSetup();
|
||||
|
||||
// PIN setup successful - now disable biometrics if it was enabled
|
||||
if (isBiometricsEnabled) {
|
||||
setIsBiometricsEnabled(false);
|
||||
await setAuthMethods(['password']);
|
||||
}
|
||||
|
||||
setPinEnabled(true);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: t('settings.vaultUnlockSettings.pinEnabled'),
|
||||
position: 'bottom',
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle cancellation or errors
|
||||
if ((error as { code?: string })?.code === 'USER_CANCELLED') {
|
||||
// User cancelled - do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Failed to enable PIN:', error);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
t('common.errors.unknownErrorTryAgain'),
|
||||
[{ text: t('common.ok'), style: 'default' }]
|
||||
);
|
||||
}
|
||||
}, [isBiometricsEnabled, setAuthMethods, t]);
|
||||
|
||||
/**
|
||||
* Handle disable PIN.
|
||||
@@ -199,84 +218,6 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
/**
|
||||
* Handle PIN change for step 1 (enter new PIN).
|
||||
*/
|
||||
const handleNewPinChange = useCallback((pin: string) : void => {
|
||||
setNewPin(pin);
|
||||
setPinError(null);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle step 1 submit (advance to confirmation).
|
||||
*/
|
||||
const handleNewPinSubmit = useCallback(() : void => {
|
||||
setPinSetupStep(2);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle PIN change for step 2 (confirm PIN).
|
||||
*/
|
||||
const handleConfirmPinChange = useCallback((pin: string) : void => {
|
||||
setConfirmPin(pin);
|
||||
setPinError(null);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle PIN setup submit.
|
||||
*/
|
||||
const handlePinSetupSubmit = useCallback(async (pin: string) : Promise<void> => {
|
||||
try {
|
||||
// Setup PIN - encryption key is retrieved internally by native code
|
||||
await setupPin(pin);
|
||||
|
||||
// PIN setup successful - now disable biometrics if it was enabled
|
||||
if (isBiometricsEnabled) {
|
||||
setIsBiometricsEnabled(false);
|
||||
await setAuthMethods(['password']);
|
||||
}
|
||||
|
||||
setPinEnabled(true);
|
||||
setShowPinSetup(false);
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setPinError(null);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: t('settings.vaultUnlockSettings.pinEnabled'),
|
||||
position: 'bottom',
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to enable PIN:', error);
|
||||
let errorMessage = t('common.errors.unknownErrorTryAgain');
|
||||
|
||||
setPinError(errorMessage);
|
||||
setConfirmPin('');
|
||||
}
|
||||
}, [isBiometricsEnabled, setAuthMethods, t]);
|
||||
|
||||
/**
|
||||
* Handle step 2 submit (confirm and save PIN).
|
||||
*/
|
||||
const handleConfirmPinSubmit = useCallback(() : void => {
|
||||
if (confirmPin !== newPin) {
|
||||
setPinError(t('settings.vaultUnlockSettings.pinMismatch'));
|
||||
// Restart from step 1 on mismatch
|
||||
setTimeout(() => {
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setPinError(null);
|
||||
}, 1000); // Show error for 1s before restarting
|
||||
return;
|
||||
}
|
||||
|
||||
// PINs match, submit
|
||||
handlePinSetupSubmit(confirmPin);
|
||||
}, [confirmPin, newPin, t, handlePinSetupSubmit]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
@@ -308,61 +249,6 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
fontSize: 13,
|
||||
marginTop: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.accentBorder,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
color: colors.text,
|
||||
fontSize: 24,
|
||||
height: 60,
|
||||
letterSpacing: 8,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
marginTop: 16,
|
||||
width: '100%',
|
||||
},
|
||||
modalButtonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalCloseButton: {
|
||||
padding: 8,
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
},
|
||||
modalContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
},
|
||||
modalText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
marginBottom: 16,
|
||||
},
|
||||
modalTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 16,
|
||||
paddingRight: 32,
|
||||
},
|
||||
option: {
|
||||
borderBottomColor: colors.accentBorder,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
@@ -471,60 +357,6 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* PIN Setup Modal */}
|
||||
<Modal
|
||||
visible={showPinSetup}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => {
|
||||
setShowPinSetup(false);
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
}}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<ThemedView style={styles.modalContent}>
|
||||
<TouchableOpacity
|
||||
style={styles.modalCloseButton}
|
||||
onPress={() => {
|
||||
setShowPinSetup(false);
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{pinSetupStep === 1 ? (
|
||||
<PinNumpad
|
||||
pin={newPin}
|
||||
onPinChange={handleNewPinChange}
|
||||
onSubmit={handleNewPinSubmit}
|
||||
error={pinError}
|
||||
title={t('settings.vaultUnlockSettings.setupPin')}
|
||||
subtitle={t('settings.vaultUnlockSettings.enterNewPinDescription')}
|
||||
submitButtonText={t('common.next')}
|
||||
minLength={4}
|
||||
maxLength={8}
|
||||
/>
|
||||
) : (
|
||||
<PinNumpad
|
||||
pin={confirmPin}
|
||||
onPinChange={handleConfirmPinChange}
|
||||
onSubmit={handleConfirmPinSubmit}
|
||||
error={pinError}
|
||||
title={t('settings.vaultUnlockSettings.confirmPin')}
|
||||
subtitle={t('settings.vaultUnlockSettings.confirmPinDescription')}
|
||||
submitButtonText={t('common.confirm')}
|
||||
minLength={4}
|
||||
maxLength={8}
|
||||
/>
|
||||
)}
|
||||
</ThemedView>
|
||||
</View>
|
||||
</Modal>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface Spec extends TurboModule {
|
||||
setupPin(pin: string): Promise<void>;
|
||||
removeAndDisablePin(): Promise<void>;
|
||||
showPinUnlockUI(): Promise<void>;
|
||||
showNativePinSetup(): Promise<void>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('NativeVaultManager');
|
||||
|
||||
Reference in New Issue
Block a user