Skip to content

Commit e34ca26

Browse files
authored
Add password obscuring support to PilotTextField (#35)
Adds textObfuscationMode parameter to PilotTextField to enable show/hide password functionality on both platforms. Android uses PasswordVisualTransformation, iOS uses dual SecureField/TextField approach with opacity switching for stable focus management. Co-authored-by: Claude Sonnet
1 parent 9635449 commit e34ca26

6 files changed

Lines changed: 103 additions & 27 deletions

File tree

components/android/material3/src/main/kotlin/com/mirego/pilot/components/ui/material3/PilotTextField.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import androidx.compose.ui.semantics.semantics
2020
import androidx.compose.ui.text.TextStyle
2121
import androidx.compose.ui.text.input.PasswordVisualTransformation
2222
import com.mirego.pilot.components.PilotTextField
23-
import com.mirego.pilot.components.type.PilotTextContentType
23+
import com.mirego.pilot.components.type.PilotTextObfuscationMode
2424
import com.mirego.pilot.components.ui.PilotFormattedVisualTransformation
2525
import com.mirego.pilot.components.ui.mergeWith
2626
import com.mirego.pilot.components.ui.type.composeValue
@@ -55,13 +55,19 @@ public fun PilotTextField(
5555
val keyboardType by pilotTextField.keyboardType.collectAsState()
5656
val keyboardReturnKeyType by pilotTextField.keyboardReturnKeyType.collectAsState()
5757
val textContentType by pilotTextField.contentType.collectAsState()
58+
val textObfuscationMode by pilotTextField.textObfuscationMode.collectAsState()
5859

5960
val modifierWithSemantics = textContentType.composeValue?.let { composeContentType ->
6061
modifier.semantics {
6162
contentType = composeContentType
6263
}
6364
} ?: modifier
6465

66+
val visualTransformation = when (textObfuscationMode) {
67+
PilotTextObfuscationMode.Hidden -> PasswordVisualTransformation()
68+
PilotTextObfuscationMode.Visible -> PilotFormattedVisualTransformation(pilotTextField.formatText)
69+
}
70+
6571
OutlinedTextField(
6672
value = textValue,
6773
onValueChange = pilotTextField::onValueChange,
@@ -80,10 +86,7 @@ public fun PilotTextField(
8086
prefix = prefix,
8187
suffix = suffix,
8288
supportingText = supportingText,
83-
visualTransformation = when (textContentType) {
84-
PilotTextContentType.Password, PilotTextContentType.NewPassword -> PasswordVisualTransformation()
85-
else -> PilotFormattedVisualTransformation(pilotTextField.formatText)
86-
},
89+
visualTransformation = visualTransformation,
8790
isError = isError,
8891
keyboardActions = pilotTextField.mergeWith(keyboardActions),
8992
keyboardOptions = KeyboardOptions(

components/common/api/components.api

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ public final class com/mirego/pilot/components/PilotSwitch {
165165

166166
public class com/mirego/pilot/components/PilotTextField {
167167
public static final field $stable I
168-
public fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
169-
public synthetic fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
168+
public fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
169+
public synthetic fun <init> (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
170170
public final fun getAutoCapitalization ()Lkotlinx/coroutines/flow/StateFlow;
171171
public final fun getAutoCorrect ()Lkotlinx/coroutines/flow/StateFlow;
172172
public final fun getContentType ()Lkotlinx/coroutines/flow/StateFlow;
@@ -176,6 +176,7 @@ public class com/mirego/pilot/components/PilotTextField {
176176
public final fun getOnReturnKeyTap ()Lkotlin/jvm/functions/Function0;
177177
public final fun getPlaceholder ()Lkotlinx/coroutines/flow/StateFlow;
178178
public final fun getText ()Lkotlinx/coroutines/flow/StateFlow;
179+
public final fun getTextObfuscationMode ()Lkotlinx/coroutines/flow/StateFlow;
179180
public final fun getTransformText ()Lkotlin/jvm/functions/Function1;
180181
public final fun getUnformatText ()Lkotlin/jvm/functions/Function1;
181182
public final fun onValueChange (Ljava/lang/String;)V
@@ -414,6 +415,14 @@ public final class com/mirego/pilot/components/type/PilotTextContentType : java/
414415
public static fun values ()[Lcom/mirego/pilot/components/type/PilotTextContentType;
415416
}
416417

418+
public final class com/mirego/pilot/components/type/PilotTextObfuscationMode : java/lang/Enum {
419+
public static final field Hidden Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
420+
public static final field Visible Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
421+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
422+
public static fun valueOf (Ljava/lang/String;)Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
423+
public static fun values ()[Lcom/mirego/pilot/components/type/PilotTextObfuscationMode;
424+
}
425+
417426
public final class com/mirego/pilot/components/ui/DefaultPilotImageResourceProvider : com/mirego/pilot/components/ui/PilotImageResourceProvider {
418427
public static final field $stable I
419428
public fun <init> ()V

components/common/src/commonMain/kotlin/com/mirego/pilot/components/PilotTextField.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.mirego.pilot.components.type.PilotKeyboardAutoCapitalization
44
import com.mirego.pilot.components.type.PilotKeyboardReturnKeyType
55
import com.mirego.pilot.components.type.PilotKeyboardType
66
import com.mirego.pilot.components.type.PilotTextContentType
7+
import com.mirego.pilot.components.type.PilotTextObfuscationMode
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.StateFlow
910

@@ -13,6 +14,7 @@ public open class PilotTextField(
1314
public val keyboardType: StateFlow<PilotKeyboardType> = MutableStateFlow(PilotKeyboardType.Default),
1415
public val keyboardReturnKeyType: StateFlow<PilotKeyboardReturnKeyType> = MutableStateFlow(PilotKeyboardReturnKeyType.Default),
1516
public val contentType: StateFlow<PilotTextContentType> = MutableStateFlow(PilotTextContentType.NotSet),
17+
public val textObfuscationMode: StateFlow<PilotTextObfuscationMode> = MutableStateFlow(PilotTextObfuscationMode.Visible),
1618
public val autoCorrect: StateFlow<Boolean> = MutableStateFlow(true),
1719
public val autoCapitalization: StateFlow<PilotKeyboardAutoCapitalization> = MutableStateFlow(PilotKeyboardAutoCapitalization.Sentences),
1820
public val onReturnKeyTap: () -> Unit = {},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.mirego.pilot.components.type
2+
3+
/**
4+
* Defines how text should be obscured in a secure text field.
5+
*/
6+
public enum class PilotTextObfuscationMode {
7+
/**
8+
* All characters are obscured (replaced with bullets/dots).
9+
*/
10+
Hidden,
11+
12+
/**
13+
* All characters are visible as plain text.
14+
*/
15+
Visible,
16+
}

components/ios/base/PilotTextFieldView.swift

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ public struct PilotTextFieldView<Label>: View where Label: View {
1010
@ObservedObject private var keyboardType: StateObservable<PilotKeyboardType>
1111
@ObservedObject private var keyboardReturnKeyType: StateObservable<PilotKeyboardReturnKeyType>
1212
@ObservedObject private var contentType: StateObservable<PilotTextContentType>
13+
@ObservedObject private var textObfuscationMode: StateObservable<PilotTextObfuscationMode>
1314
@ObservedObject private var autoCorrect: StateObservable<KotlinBoolean>
1415
@ObservedObject private var autoCapitalization: StateObservable<PilotKeyboardAutoCapitalization>
1516

1617
@State private var textFieldText: String
18+
@FocusState private var focusedField: FieldType?
19+
20+
private enum FieldType {
21+
case secure
22+
case plain
23+
}
1724

1825
public init(_ pilotTextField: PilotTextField, placeholderBuilder: @escaping (String) -> Label) {
1926
self.pilotTextField = pilotTextField
@@ -24,34 +31,60 @@ public struct PilotTextFieldView<Label>: View where Label: View {
2431
_keyboardType = ObservedObject(wrappedValue: StateObservable(pilotTextField.keyboardType))
2532
_keyboardReturnKeyType = ObservedObject(wrappedValue: StateObservable(pilotTextField.keyboardReturnKeyType))
2633
_contentType = ObservedObject(wrappedValue: StateObservable(pilotTextField.contentType))
34+
_textObfuscationMode = ObservedObject(wrappedValue: StateObservable(pilotTextField.textObfuscationMode))
2735
_autoCorrect = ObservedObject(wrappedValue: StateObservable(pilotTextField.autoCorrect))
2836
_autoCapitalization = ObservedObject(wrappedValue: StateObservable(pilotTextField.autoCapitalization))
2937

3038
_textFieldText = State(initialValue: pilotTextField.formatText(pilotTextField.transformText(pilotTextField.text.value)))
3139
}
3240

3341
public var body: some View {
34-
TextField(text: $textFieldText) {
35-
placeholderBuilder(placeholder.value)
36-
}
37-
.onSubmit {
38-
pilotTextField.onReturnKeyTap()
39-
}
40-
.submitLabel(keyboardReturnKeyType.value.submitLabel)
41-
#if canImport(UIKit)
42-
.keyboardType(keyboardType.value.uiKeyboardType)
43-
.autocapitalization(autoCapitalization.value.uiTextAutocapitalizationType)
44-
.textContentType(contentType.value.uiTextContentType)
45-
#endif
46-
.disableAutocorrection(!autoCorrect.value.boolValue)
47-
.textFieldStyle(ExtendedTapAreaTextFieldStyle())
48-
.onChange(of: text.value) { newValue in
49-
textFieldText = pilotTextField.formatText(newValue)
42+
baseTextField
43+
.onSubmit {
44+
pilotTextField.onReturnKeyTap()
45+
}
46+
.submitLabel(keyboardReturnKeyType.value.submitLabel)
47+
#if canImport(UIKit)
48+
.keyboardType(keyboardType.value.uiKeyboardType)
49+
.autocapitalization(autoCapitalization.value.uiTextAutocapitalizationType)
50+
.textContentType(contentType.value.uiTextContentType)
51+
#endif
52+
.disableAutocorrection(!autoCorrect.value.boolValue)
53+
.textFieldStyle(ExtendedTapAreaTextFieldStyle())
54+
.onChange(of: text.value) { newValue in
55+
textFieldText = pilotTextField.formatText(newValue)
56+
}
57+
.onChange(of: textFieldText) { newValue in
58+
let unformattedText = pilotTextField.unformatText(newValue)
59+
textFieldText = pilotTextField.formatText(pilotTextField.transformText(unformattedText))
60+
pilotTextField.onValueChange(text: unformattedText)
61+
}
62+
.onChange(of: textObfuscationMode.value) { newValue in
63+
if focusedField != nil {
64+
withAnimation(nil) {
65+
focusedField = newValue == .hidden ? .secure : .plain
66+
}
67+
}
68+
}
69+
}
70+
71+
@ViewBuilder
72+
private var baseTextField: some View {
73+
ZStack {
74+
SecureField(text: $textFieldText) {
75+
placeholderBuilder(placeholder.value)
76+
}
77+
.opacity(textObfuscationMode.value == .hidden ? 1 : 0)
78+
.focused($focusedField, equals: .secure)
79+
80+
TextField(text: $textFieldText) {
81+
placeholderBuilder(placeholder.value)
82+
}
83+
.opacity(textObfuscationMode.value == .visible ? 1 : 0)
84+
.focused($focusedField, equals: .plain)
5085
}
51-
.onChange(of: textFieldText) { newValue in
52-
let unformattedText = pilotTextField.unformatText(newValue)
53-
textFieldText = pilotTextField.formatText(pilotTextField.transformText(unformattedText))
54-
pilotTextField.onValueChange(text: unformattedText)
86+
.transaction { transaction in
87+
transaction.animation = nil
5588
}
5689
}
5790
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Shared
2+
import SwiftUI
3+
4+
extension PilotTextObfuscationMode: Equatable {
5+
public static func == (lhs: PilotTextObfuscationMode, rhs: PilotTextObfuscationMode) -> Bool {
6+
switch (lhs, rhs) {
7+
case (.hidden, .hidden), (.visible, .visible):
8+
return true
9+
default:
10+
return false
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)