@entropy-softworks/ui 2026.8.22 → 2026.8.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,17 +11,15 @@ interface LockOverlayProps {
|
|
|
11
11
|
successMessage?: string;
|
|
12
12
|
/**
|
|
13
13
|
* Optional biometric/passkey unlock. When provided, this fires
|
|
14
|
-
* AUTOMATICALLY once on mount
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* would fire immediately and unprompted on every single mount since
|
|
24
|
-
* this path is now automatic.
|
|
14
|
+
* AUTOMATICALLY once on mount, and the password field/Unlock button
|
|
15
|
+
* are not rendered at all until this settles — see the mode-switching
|
|
16
|
+
* comment below for why. This overlay has no opinion on *how* unlock
|
|
17
|
+
* happens (passkey PRF, platform secure-storage, etc.) — the caller
|
|
18
|
+
* owns the whole ceremony and is expected to swallow expected failure
|
|
19
|
+
* modes (user cancel, nothing enrolled) rather than throw; a throw
|
|
20
|
+
* here surfaces the same generic "unlock failed" error the password
|
|
21
|
+
* path does, which is the wrong message for "you tapped cancel on the
|
|
22
|
+
* Face ID prompt".
|
|
25
23
|
*/
|
|
26
24
|
onBiometricUnlock?: () => Promise<void> | void;
|
|
27
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LockOverlay.d.ts","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAiB3D,UAAU,gBAAgB;IACxB;;;OAGG;IACH,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACrD,4CAA4C;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB
|
|
1
|
+
{"version":3,"file":"LockOverlay.d.ts","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAiB3D,UAAU,gBAAgB;IACxB;;;OAGG;IACH,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACrD,4CAA4C;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAChD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,cAAc,EACd,iBAAiB,GAClB,EAAE,gBAAgB,qBA0NlB"}
|
|
@@ -23,8 +23,28 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
23
23
|
const [loading, setLoading] = useState(false);
|
|
24
24
|
const [biometricState, setBiometricState] = useState("idle");
|
|
25
25
|
const [error, setError] = useState("");
|
|
26
|
-
const hasAutoPrompted = useRef(false);
|
|
27
26
|
const pulse = useSharedValue(1);
|
|
27
|
+
const hasAutoPrompted = useRef(false);
|
|
28
|
+
// Two distinct screens, not one screen with an extra button:
|
|
29
|
+
// "biometric" — fingerprint icon only. No password field, no Unlock
|
|
30
|
+
// button anywhere in the tree.
|
|
31
|
+
// "password" — the normal password form, shown once biometric
|
|
32
|
+
// unlock isn't offered at all, OR once an attempt has
|
|
33
|
+
// settled (succeeded or failed — a real success
|
|
34
|
+
// unmounts this whole overlay from the parent almost
|
|
35
|
+
// immediately, so briefly being in "password" mode in
|
|
36
|
+
// that case is never visible).
|
|
37
|
+
// The password field being ABSENT from the tree during "biometric"
|
|
38
|
+
// mode is load-bearing, not cosmetic: Android's Autofill Framework
|
|
39
|
+
// triggers independently of the passkey ceremony, purely from a
|
|
40
|
+
// password-type field being mounted and focusable on screen. With
|
|
41
|
+
// both mounted together, the passkey ceremony's own Credential
|
|
42
|
+
// Manager UI and Bitwarden's separately-triggered autofill prompt
|
|
43
|
+
// fought each other — dismiss one, the other reopens immediately,
|
|
44
|
+
// neither settling — reported as a black screen the app never
|
|
45
|
+
// recovered from, root-caused on 2026-08-17. There is no such field
|
|
46
|
+
// for Android to react to until biometric unlock has actually failed.
|
|
47
|
+
const [mode, setMode] = useState(onBiometricUnlock ? "biometric" : "password");
|
|
28
48
|
const triggerBiometricUnlock = React.useCallback(async () => {
|
|
29
49
|
if (!onBiometricUnlock) {
|
|
30
50
|
return;
|
|
@@ -35,13 +55,9 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
35
55
|
}
|
|
36
56
|
finally {
|
|
37
57
|
setBiometricState("idle");
|
|
58
|
+
setMode("password");
|
|
38
59
|
}
|
|
39
60
|
}, [onBiometricUnlock]);
|
|
40
|
-
// Fires once, automatically, the moment this screen can offer it — no
|
|
41
|
-
// button to tap first. A stale-closure-safe ref guard (not a `[]]`
|
|
42
|
-
// dependency array alone) because triggerBiometricUnlock's identity
|
|
43
|
-
// can change across the caller's own re-renders before the first
|
|
44
|
-
// prompt has actually happened.
|
|
45
61
|
useEffect(() => {
|
|
46
62
|
if (onBiometricUnlock && !hasAutoPrompted.current) {
|
|
47
63
|
hasAutoPrompted.current = true;
|
|
@@ -93,6 +109,27 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
93
109
|
setLoading(false);
|
|
94
110
|
}
|
|
95
111
|
};
|
|
112
|
+
if (mode === "biometric") {
|
|
113
|
+
return (<View className="flex-1 items-center justify-center bg-background dark:bg-black px-6">
|
|
114
|
+
<View className="mb-8 flex-row items-center justify-center">
|
|
115
|
+
<Logo width={56} height={56}/>
|
|
116
|
+
<Text className="font-sans-bold text-[28px] text-foreground dark:text-white">
|
|
117
|
+
{appName}
|
|
118
|
+
</Text>
|
|
119
|
+
</View>
|
|
120
|
+
<Pressable onPress={triggerBiometricUnlock} accessibilityRole="button" accessibilityLabel="Unlock with biometrics" testID="lock-biometric-icon" className="items-center">
|
|
121
|
+
<Animated.View style={pulseStyle} className="h-24 w-24 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900">
|
|
122
|
+
<Ionicons name="finger-print" size={52} color={isDark ? "#FFFFFF" : "#000000"}/>
|
|
123
|
+
</Animated.View>
|
|
124
|
+
<MutedText className="mt-3 text-sm">
|
|
125
|
+
{biometricState === "prompting" ? "Authenticating…" : "Tap to unlock"}
|
|
126
|
+
</MutedText>
|
|
127
|
+
</Pressable>
|
|
128
|
+
<Text className="mt-10 text-xs text-muted-foreground dark:text-neutral-500" onPress={onSignOut} accessibilityRole="button" accessibilityLabel="Sign out">
|
|
129
|
+
Sign out
|
|
130
|
+
</Text>
|
|
131
|
+
</View>);
|
|
132
|
+
}
|
|
96
133
|
return (<View className="flex-1 items-center justify-center bg-background dark:bg-black">
|
|
97
134
|
<View className="w-full max-w-[340px] items-center px-6">
|
|
98
135
|
<View className="mb-2 flex-row items-center justify-center">
|
|
@@ -105,21 +142,10 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
105
142
|
Enter your master password to unlock
|
|
106
143
|
</MutedText>
|
|
107
144
|
|
|
108
|
-
{onBiometricUnlock ? (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</Animated.View>
|
|
113
|
-
<MutedText className="mt-2 text-xs">
|
|
114
|
-
{biometricState === "prompting" ? "Authenticating…" : "Tap to try again"}
|
|
115
|
-
</MutedText>
|
|
116
|
-
</Pressable>
|
|
117
|
-
<View className="mb-3 w-full flex-row items-center gap-3">
|
|
118
|
-
<View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
|
|
119
|
-
<MutedText className="text-xs">or use your password</MutedText>
|
|
120
|
-
<View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
|
|
121
|
-
</View>
|
|
122
|
-
</>) : null}
|
|
145
|
+
{onBiometricUnlock ? (<Pressable onPress={() => setMode("biometric")} accessibilityRole="button" accessibilityLabel="Try biometrics again" className="mb-6 flex-row items-center gap-2">
|
|
146
|
+
<Ionicons name="finger-print" size={16} color={isDark ? "#A3A3A3" : "#737373"}/>
|
|
147
|
+
<MutedText className="text-xs">Try biometrics again</MutedText>
|
|
148
|
+
</Pressable>) : null}
|
|
123
149
|
|
|
124
150
|
<View className="w-full gap-3">
|
|
125
151
|
<PasswordInput value={password} onChangeText={(text) => {
|
|
@@ -129,12 +155,10 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
129
155
|
}
|
|
130
156
|
}} placeholder="Master password" error={error}
|
|
131
157
|
// Web: autofocus on a login-style page is expected desktop
|
|
132
|
-
// UX. Native:
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// reported as a real annoyance on 2026-08-17. Unlock should
|
|
137
|
-
// wait for an explicit tap on the field.
|
|
158
|
+
// UX. Native: autofocus here pops the on-screen keyboard
|
|
159
|
+
// unprompted the moment the password fallback appears —
|
|
160
|
+
// reported as a real annoyance previously. Wait for an
|
|
161
|
+
// explicit tap on the field.
|
|
138
162
|
autoFocus={Platform.OS === "web"} returnKeyType="done" onSubmitEditing={handleUnlock} accessibilityLabel="Master password" testID="lock-password-input"/>
|
|
139
163
|
<Button className="w-full" onPress={handleUnlock} isLoading={loading} accessibilityLabel="Unlock" accessibilityRole="button" testID="lock-unlock-button">
|
|
140
164
|
Unlock
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LockOverlay.jsx","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,MAAM,4BAA4B,CAAC;AAC/C,OAAO,QAAQ,EAAE,EACf,MAAM,EACN,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,UAAU,GACX,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"LockOverlay.jsx","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -27,17 +27,15 @@ interface LockOverlayProps {
|
|
|
27
27
|
successMessage?: string;
|
|
28
28
|
/**
|
|
29
29
|
* Optional biometric/passkey unlock. When provided, this fires
|
|
30
|
-
* AUTOMATICALLY once on mount
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* would fire immediately and unprompted on every single mount since
|
|
40
|
-
* this path is now automatic.
|
|
30
|
+
* AUTOMATICALLY once on mount, and the password field/Unlock button
|
|
31
|
+
* are not rendered at all until this settles — see the mode-switching
|
|
32
|
+
* comment below for why. This overlay has no opinion on *how* unlock
|
|
33
|
+
* happens (passkey PRF, platform secure-storage, etc.) — the caller
|
|
34
|
+
* owns the whole ceremony and is expected to swallow expected failure
|
|
35
|
+
* modes (user cancel, nothing enrolled) rather than throw; a throw
|
|
36
|
+
* here surfaces the same generic "unlock failed" error the password
|
|
37
|
+
* path does, which is the wrong message for "you tapped cancel on the
|
|
38
|
+
* Face ID prompt".
|
|
41
39
|
*/
|
|
42
40
|
onBiometricUnlock?: () => Promise<void> | void;
|
|
43
41
|
}
|
|
@@ -62,8 +60,31 @@ export function LockOverlay({
|
|
|
62
60
|
const [loading, setLoading] = useState(false);
|
|
63
61
|
const [biometricState, setBiometricState] = useState<"idle" | "prompting">("idle");
|
|
64
62
|
const [error, setError] = useState("");
|
|
65
|
-
const hasAutoPrompted = useRef(false);
|
|
66
63
|
const pulse = useSharedValue(1);
|
|
64
|
+
const hasAutoPrompted = useRef(false);
|
|
65
|
+
|
|
66
|
+
// Two distinct screens, not one screen with an extra button:
|
|
67
|
+
// "biometric" — fingerprint icon only. No password field, no Unlock
|
|
68
|
+
// button anywhere in the tree.
|
|
69
|
+
// "password" — the normal password form, shown once biometric
|
|
70
|
+
// unlock isn't offered at all, OR once an attempt has
|
|
71
|
+
// settled (succeeded or failed — a real success
|
|
72
|
+
// unmounts this whole overlay from the parent almost
|
|
73
|
+
// immediately, so briefly being in "password" mode in
|
|
74
|
+
// that case is never visible).
|
|
75
|
+
// The password field being ABSENT from the tree during "biometric"
|
|
76
|
+
// mode is load-bearing, not cosmetic: Android's Autofill Framework
|
|
77
|
+
// triggers independently of the passkey ceremony, purely from a
|
|
78
|
+
// password-type field being mounted and focusable on screen. With
|
|
79
|
+
// both mounted together, the passkey ceremony's own Credential
|
|
80
|
+
// Manager UI and Bitwarden's separately-triggered autofill prompt
|
|
81
|
+
// fought each other — dismiss one, the other reopens immediately,
|
|
82
|
+
// neither settling — reported as a black screen the app never
|
|
83
|
+
// recovered from, root-caused on 2026-08-17. There is no such field
|
|
84
|
+
// for Android to react to until biometric unlock has actually failed.
|
|
85
|
+
const [mode, setMode] = useState<"biometric" | "password">(
|
|
86
|
+
onBiometricUnlock ? "biometric" : "password"
|
|
87
|
+
);
|
|
67
88
|
|
|
68
89
|
const triggerBiometricUnlock = React.useCallback(async () => {
|
|
69
90
|
if (!onBiometricUnlock) {
|
|
@@ -74,14 +95,10 @@ export function LockOverlay({
|
|
|
74
95
|
await onBiometricUnlock();
|
|
75
96
|
} finally {
|
|
76
97
|
setBiometricState("idle");
|
|
98
|
+
setMode("password");
|
|
77
99
|
}
|
|
78
100
|
}, [onBiometricUnlock]);
|
|
79
101
|
|
|
80
|
-
// Fires once, automatically, the moment this screen can offer it — no
|
|
81
|
-
// button to tap first. A stale-closure-safe ref guard (not a `[]]`
|
|
82
|
-
// dependency array alone) because triggerBiometricUnlock's identity
|
|
83
|
-
// can change across the caller's own re-renders before the first
|
|
84
|
-
// prompt has actually happened.
|
|
85
102
|
useEffect(() => {
|
|
86
103
|
if (onBiometricUnlock && !hasAutoPrompted.current) {
|
|
87
104
|
hasAutoPrompted.current = true;
|
|
@@ -141,6 +158,44 @@ export function LockOverlay({
|
|
|
141
158
|
}
|
|
142
159
|
};
|
|
143
160
|
|
|
161
|
+
if (mode === "biometric") {
|
|
162
|
+
return (
|
|
163
|
+
<View className="flex-1 items-center justify-center bg-background dark:bg-black px-6">
|
|
164
|
+
<View className="mb-8 flex-row items-center justify-center">
|
|
165
|
+
<Logo width={56} height={56} />
|
|
166
|
+
<Text className="font-sans-bold text-[28px] text-foreground dark:text-white">
|
|
167
|
+
{appName}
|
|
168
|
+
</Text>
|
|
169
|
+
</View>
|
|
170
|
+
<Pressable
|
|
171
|
+
onPress={triggerBiometricUnlock}
|
|
172
|
+
accessibilityRole="button"
|
|
173
|
+
accessibilityLabel="Unlock with biometrics"
|
|
174
|
+
testID="lock-biometric-icon"
|
|
175
|
+
className="items-center"
|
|
176
|
+
>
|
|
177
|
+
<Animated.View
|
|
178
|
+
style={pulseStyle}
|
|
179
|
+
className="h-24 w-24 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900"
|
|
180
|
+
>
|
|
181
|
+
<Ionicons name="finger-print" size={52} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
182
|
+
</Animated.View>
|
|
183
|
+
<MutedText className="mt-3 text-sm">
|
|
184
|
+
{biometricState === "prompting" ? "Authenticating…" : "Tap to unlock"}
|
|
185
|
+
</MutedText>
|
|
186
|
+
</Pressable>
|
|
187
|
+
<Text
|
|
188
|
+
className="mt-10 text-xs text-muted-foreground dark:text-neutral-500"
|
|
189
|
+
onPress={onSignOut}
|
|
190
|
+
accessibilityRole="button"
|
|
191
|
+
accessibilityLabel="Sign out"
|
|
192
|
+
>
|
|
193
|
+
Sign out
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
144
199
|
return (
|
|
145
200
|
<View className="flex-1 items-center justify-center bg-background dark:bg-black">
|
|
146
201
|
<View className="w-full max-w-[340px] items-center px-6">
|
|
@@ -155,30 +210,19 @@ export function LockOverlay({
|
|
|
155
210
|
</MutedText>
|
|
156
211
|
|
|
157
212
|
{onBiometricUnlock ? (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</Animated.View>
|
|
172
|
-
<MutedText className="mt-2 text-xs">
|
|
173
|
-
{biometricState === "prompting" ? "Authenticating…" : "Tap to try again"}
|
|
174
|
-
</MutedText>
|
|
175
|
-
</Pressable>
|
|
176
|
-
<View className="mb-3 w-full flex-row items-center gap-3">
|
|
177
|
-
<View className="h-px flex-1 bg-border dark:bg-neutral-800" />
|
|
178
|
-
<MutedText className="text-xs">or use your password</MutedText>
|
|
179
|
-
<View className="h-px flex-1 bg-border dark:bg-neutral-800" />
|
|
180
|
-
</View>
|
|
181
|
-
</>
|
|
213
|
+
<Pressable
|
|
214
|
+
onPress={() => setMode("biometric")}
|
|
215
|
+
accessibilityRole="button"
|
|
216
|
+
accessibilityLabel="Try biometrics again"
|
|
217
|
+
className="mb-6 flex-row items-center gap-2"
|
|
218
|
+
>
|
|
219
|
+
<Ionicons
|
|
220
|
+
name="finger-print"
|
|
221
|
+
size={16}
|
|
222
|
+
color={isDark ? "#A3A3A3" : "#737373"}
|
|
223
|
+
/>
|
|
224
|
+
<MutedText className="text-xs">Try biometrics again</MutedText>
|
|
225
|
+
</Pressable>
|
|
182
226
|
) : null}
|
|
183
227
|
|
|
184
228
|
<View className="w-full gap-3">
|
|
@@ -193,12 +237,10 @@ export function LockOverlay({
|
|
|
193
237
|
placeholder="Master password"
|
|
194
238
|
error={error}
|
|
195
239
|
// Web: autofocus on a login-style page is expected desktop
|
|
196
|
-
// UX. Native:
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// reported as a real annoyance on 2026-08-17. Unlock should
|
|
201
|
-
// wait for an explicit tap on the field.
|
|
240
|
+
// UX. Native: autofocus here pops the on-screen keyboard
|
|
241
|
+
// unprompted the moment the password fallback appears —
|
|
242
|
+
// reported as a real annoyance previously. Wait for an
|
|
243
|
+
// explicit tap on the field.
|
|
202
244
|
autoFocus={Platform.OS === "web"}
|
|
203
245
|
returnKeyType="done"
|
|
204
246
|
onSubmitEditing={handleUnlock}
|