@entropy-softworks/ui 2026.8.23 → 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 (no button to tap — a fingerprint icon
15
- * is the affordance, tappable to retry after a failed/cancelled
16
- * attempt) rather than waiting for the user to press anything. This
17
- * overlay has no opinion on *how* unlock happens (passkey PRF,
18
- * platform secure-storage, etc.) the caller owns the whole ceremony
19
- * and is expected to swallow expected failure modes (user cancel,
20
- * nothing enrolled) rather than throw; a throw here surfaces the same
21
- * generic "unlock failed" error the password path does, which is the
22
- * wrong message for "you tapped cancel on the Face ID prompt", and
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,KAA8B,MAAM,OAAO,CAAC;AAiBnD,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;;;;;;;;;;;;;OAaG;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,qBA8KlB"}
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"}
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { Platform, Pressable, View } from "react-native";
3
3
  import { Ionicons } from "@expo/vector-icons";
4
4
  import Toast from "react-native-toast-message";
@@ -24,19 +24,27 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
24
24
  const [biometricState, setBiometricState] = useState("idle");
25
25
  const [error, setError] = useState("");
26
26
  const pulse = useSharedValue(1);
27
- // Fires only on explicit tap of the fingerprint icon below — NOT
28
- // automatically on mount (that was here briefly). On Android, a
29
- // passkey/WebAuthn ceremony goes through the OS's Credential Manager,
30
- // which surfaces EVERY registered credential provider on the device —
31
- // including other password managers like Bitwarden if one is set as
32
- // an autofill/credential provider. Firing that automatically on every
33
- // single mount raced against Bitwarden's own autofill prompt for this
34
- // app, the two fighting each other: dismiss one, the other reopens
35
- // immediately, with vault's own UI never getting a chance to paint
36
- // underneath — reported as a black screen that wouldn't go away, on
37
- // 2026-08-17. Requiring an explicit tap avoids ever entering that
38
- // credential-provider arbitration unless the user actually asked for
39
- // it just now.
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");
40
48
  const triggerBiometricUnlock = React.useCallback(async () => {
41
49
  if (!onBiometricUnlock) {
42
50
  return;
@@ -47,8 +55,15 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
47
55
  }
48
56
  finally {
49
57
  setBiometricState("idle");
58
+ setMode("password");
50
59
  }
51
60
  }, [onBiometricUnlock]);
61
+ useEffect(() => {
62
+ if (onBiometricUnlock && !hasAutoPrompted.current) {
63
+ hasAutoPrompted.current = true;
64
+ void triggerBiometricUnlock();
65
+ }
66
+ }, [onBiometricUnlock, triggerBiometricUnlock]);
52
67
  useEffect(() => {
53
68
  if (biometricState === "prompting") {
54
69
  pulse.value = withRepeat(withTiming(1.12, { duration: 650, easing: Easing.inOut(Easing.ease) }), -1, true);
@@ -94,6 +109,27 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
94
109
  setLoading(false);
95
110
  }
96
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
+ }
97
133
  return (<View className="flex-1 items-center justify-center bg-background dark:bg-black">
98
134
  <View className="w-full max-w-[340px] items-center px-6">
99
135
  <View className="mb-2 flex-row items-center justify-center">
@@ -106,21 +142,10 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
106
142
  Enter your master password to unlock
107
143
  </MutedText>
108
144
 
109
- {onBiometricUnlock ? (<>
110
- <Pressable onPress={triggerBiometricUnlock} accessibilityRole="button" accessibilityLabel="Unlock with biometrics" testID="lock-biometric-icon" className="mb-3 items-center">
111
- <Animated.View style={pulseStyle} className="h-20 w-20 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900">
112
- <Ionicons name="finger-print" size={44} color={isDark ? "#FFFFFF" : "#000000"}/>
113
- </Animated.View>
114
- <MutedText className="mt-2 text-xs">
115
- {biometricState === "prompting" ? "Authenticating…" : "Tap to unlock"}
116
- </MutedText>
117
- </Pressable>
118
- <View className="mb-3 w-full flex-row items-center gap-3">
119
- <View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
120
- <MutedText className="text-xs">or use your password</MutedText>
121
- <View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
122
- </View>
123
- </>) : 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}
124
149
 
125
150
  <View className="w-full gap-3">
126
151
  <PasswordInput value={password} onChangeText={(text) => {
@@ -130,12 +155,10 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
130
155
  }
131
156
  }} placeholder="Master password" error={error}
132
157
  // Web: autofocus on a login-style page is expected desktop
133
- // UX. Native: this overlay mounts unconditionally at app
134
- // launch (and can be visible under the OTA install overlay,
135
- // before the user has done anything at all), so autofocus
136
- // pops the on-screen keyboard immediately and unprompted —
137
- // reported as a real annoyance on 2026-08-17. Unlock should
138
- // 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.
139
162
  autoFocus={Platform.OS === "web"} returnKeyType="done" onSubmitEditing={handleUnlock} accessibilityLabel="Master password" testID="lock-password-input"/>
140
163
  <Button className="w-full" onPress={handleUnlock} isLoading={loading} accessibilityLabel="Unlock" accessibilityRole="button" testID="lock-unlock-button">
141
164
  Unlock
@@ -1 +1 @@
1
- {"version":3,"file":"LockOverlay.jsx","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LockOverlay.jsx","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entropy-softworks/ui",
3
- "version": "2026.8.23",
3
+ "version": "2026.8.24",
4
4
  "private": false,
5
5
  "description": "Shared React Native UI primitives, auth screens, hooks, and services for Entropy Softworks Expo apps",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { Platform, Pressable, View } from "react-native";
3
3
  import { Ionicons } from "@expo/vector-icons";
4
4
  import Toast from "react-native-toast-message";
@@ -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 (no button to tap — a fingerprint icon
31
- * is the affordance, tappable to retry after a failed/cancelled
32
- * attempt) rather than waiting for the user to press anything. This
33
- * overlay has no opinion on *how* unlock happens (passkey PRF,
34
- * platform secure-storage, etc.) the caller owns the whole ceremony
35
- * and is expected to swallow expected failure modes (user cancel,
36
- * nothing enrolled) rather than throw; a throw here surfaces the same
37
- * generic "unlock failed" error the password path does, which is the
38
- * wrong message for "you tapped cancel on the Face ID prompt", and
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
  }
@@ -63,20 +61,31 @@ export function LockOverlay({
63
61
  const [biometricState, setBiometricState] = useState<"idle" | "prompting">("idle");
64
62
  const [error, setError] = useState("");
65
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
+ );
66
88
 
67
- // Fires only on explicit tap of the fingerprint icon below — NOT
68
- // automatically on mount (that was here briefly). On Android, a
69
- // passkey/WebAuthn ceremony goes through the OS's Credential Manager,
70
- // which surfaces EVERY registered credential provider on the device —
71
- // including other password managers like Bitwarden if one is set as
72
- // an autofill/credential provider. Firing that automatically on every
73
- // single mount raced against Bitwarden's own autofill prompt for this
74
- // app, the two fighting each other: dismiss one, the other reopens
75
- // immediately, with vault's own UI never getting a chance to paint
76
- // underneath — reported as a black screen that wouldn't go away, on
77
- // 2026-08-17. Requiring an explicit tap avoids ever entering that
78
- // credential-provider arbitration unless the user actually asked for
79
- // it just now.
80
89
  const triggerBiometricUnlock = React.useCallback(async () => {
81
90
  if (!onBiometricUnlock) {
82
91
  return;
@@ -86,9 +95,17 @@ export function LockOverlay({
86
95
  await onBiometricUnlock();
87
96
  } finally {
88
97
  setBiometricState("idle");
98
+ setMode("password");
89
99
  }
90
100
  }, [onBiometricUnlock]);
91
101
 
102
+ useEffect(() => {
103
+ if (onBiometricUnlock && !hasAutoPrompted.current) {
104
+ hasAutoPrompted.current = true;
105
+ void triggerBiometricUnlock();
106
+ }
107
+ }, [onBiometricUnlock, triggerBiometricUnlock]);
108
+
92
109
  useEffect(() => {
93
110
  if (biometricState === "prompting") {
94
111
  pulse.value = withRepeat(
@@ -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
- <Pressable
160
- onPress={triggerBiometricUnlock}
161
- accessibilityRole="button"
162
- accessibilityLabel="Unlock with biometrics"
163
- testID="lock-biometric-icon"
164
- className="mb-3 items-center"
165
- >
166
- <Animated.View
167
- style={pulseStyle}
168
- className="h-20 w-20 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900"
169
- >
170
- <Ionicons name="finger-print" size={44} color={isDark ? "#FFFFFF" : "#000000"} />
171
- </Animated.View>
172
- <MutedText className="mt-2 text-xs">
173
- {biometricState === "prompting" ? "Authenticating…" : "Tap to unlock"}
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: this overlay mounts unconditionally at app
197
- // launch (and can be visible under the OTA install overlay,
198
- // before the user has done anything at all), so autofocus
199
- // pops the on-screen keyboard immediately and unprompted —
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}