@entropy-softworks/ui 2026.8.21 → 2026.8.23

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.
@@ -10,20 +10,20 @@ interface LockOverlayProps {
10
10
  /** Optional success-toast message. Defaults to `${appName} unlocked`. */
11
11
  successMessage?: string;
12
12
  /**
13
- * Optional biometric/passkey unlock, shown as a prominent button above
14
- * the password field when provided. This overlay has no opinion on
15
- * *how* that happens (passkey PRF, platform secure-storage, etc.)
16
- * the caller owns the whole ceremony and is expected to swallow
17
- * expected failure modes (user cancel, nothing enrolled) rather than
18
- * throw; a throw here surfaces the same generic "unlock failed" error
19
- * the password path does, which is the wrong message for "you tapped
20
- * cancel on the Face ID prompt".
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.
21
25
  */
22
26
  onBiometricUnlock?: () => Promise<void> | void;
23
- /** Button label. Defaults to "Unlock with Biometrics" — pass
24
- * something more specific ("Unlock with Face ID") when the caller
25
- * can detect which authentication type is actually available. */
26
- biometricLabel?: string;
27
27
  }
28
28
  /**
29
29
  * Unlock form for session-locked flows (e.g. vault re-lock after idle).
@@ -32,6 +32,6 @@ interface LockOverlayProps {
32
32
  * unlock work begins so the fiber tree no longer references the
33
33
  * plaintext during PBKDF2 / WebCrypto derivation.
34
34
  */
35
- export declare function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, biometricLabel, }: LockOverlayProps): React.JSX.Element;
35
+ export declare function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, }: LockOverlayProps): React.JSX.Element;
36
36
  export {};
37
37
  //# sourceMappingURL=LockOverlay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LockOverlay.d.ts","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAQxC,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;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/C;;sEAEkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,cAAc,GACf,EAAE,gBAAgB,qBAkIlB"}
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,6 +1,9 @@
1
- import React, { useState } from "react";
2
- import { Platform, View } from "react-native";
1
+ import React, { useEffect, useState } from "react";
2
+ import { Platform, Pressable, View } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
3
4
  import Toast from "react-native-toast-message";
5
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from "react-native-reanimated";
6
+ import { useColorScheme } from "nativewind";
4
7
  import { Button } from "../../components/button";
5
8
  import { PasswordInput } from "../../components/input";
6
9
  import { MutedText, Text } from "../../components/text";
@@ -12,21 +15,51 @@ import { useAppConfig } from "../../context/AppConfigContext";
12
15
  * unlock work begins so the fiber tree no longer references the
13
16
  * plaintext during PBKDF2 / WebCrypto derivation.
14
17
  */
15
- export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, biometricLabel, }) {
18
+ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, }) {
16
19
  const { appName, Logo } = useAppConfig();
20
+ const { colorScheme } = useColorScheme();
21
+ const isDark = colorScheme === "dark";
17
22
  const [password, setPassword] = useState("");
18
23
  const [loading, setLoading] = useState(false);
19
- const [biometricLoading, setBiometricLoading] = useState(false);
24
+ const [biometricState, setBiometricState] = useState("idle");
20
25
  const [error, setError] = useState("");
21
- const handleBiometricUnlock = async () => {
22
- setBiometricLoading(true);
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.
40
+ const triggerBiometricUnlock = React.useCallback(async () => {
41
+ if (!onBiometricUnlock) {
42
+ return;
43
+ }
44
+ setBiometricState("prompting");
23
45
  try {
24
- await onBiometricUnlock?.();
46
+ await onBiometricUnlock();
25
47
  }
26
48
  finally {
27
- setBiometricLoading(false);
49
+ setBiometricState("idle");
28
50
  }
29
- };
51
+ }, [onBiometricUnlock]);
52
+ useEffect(() => {
53
+ if (biometricState === "prompting") {
54
+ pulse.value = withRepeat(withTiming(1.12, { duration: 650, easing: Easing.inOut(Easing.ease) }), -1, true);
55
+ }
56
+ else {
57
+ pulse.value = withTiming(1, { duration: 200 });
58
+ }
59
+ }, [biometricState, pulse]);
60
+ const pulseStyle = useAnimatedStyle(() => ({
61
+ transform: [{ scale: pulse.value }],
62
+ }));
30
63
  const handleUnlock = async () => {
31
64
  const captured = password;
32
65
  if (!captured.trim()) {
@@ -72,17 +105,24 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
72
105
  <MutedText className="mb-8 text-center text-sm">
73
106
  Enter your master password to unlock
74
107
  </MutedText>
108
+
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}
124
+
75
125
  <View className="w-full gap-3">
76
- {onBiometricUnlock ? (<>
77
- <Button className="w-full" onPress={handleBiometricUnlock} isLoading={biometricLoading} accessibilityLabel={biometricLabel ?? "Unlock with Biometrics"} accessibilityRole="button" testID="lock-biometric-button">
78
- {biometricLabel ?? "Unlock with Biometrics"}
79
- </Button>
80
- <View className="flex-row items-center gap-3 py-1">
81
- <View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
82
- <MutedText className="text-xs">or use your password</MutedText>
83
- <View className="h-px flex-1 bg-border dark:bg-neutral-800"/>
84
- </View>
85
- </>) : null}
86
126
  <PasswordInput value={password} onChangeText={(text) => {
87
127
  setPassword(text);
88
128
  if (error) {
@@ -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.21",
3
+ "version": "2026.8.23",
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,6 +1,15 @@
1
- import React, { useState } from "react";
2
- import { Platform, View } from "react-native";
1
+ import React, { useEffect, useState } from "react";
2
+ import { Platform, Pressable, View } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
3
4
  import Toast from "react-native-toast-message";
5
+ import Animated, {
6
+ Easing,
7
+ useAnimatedStyle,
8
+ useSharedValue,
9
+ withRepeat,
10
+ withTiming,
11
+ } from "react-native-reanimated";
12
+ import { useColorScheme } from "nativewind";
4
13
  import { Button } from "../../components/button";
5
14
  import { PasswordInput } from "../../components/input";
6
15
  import { MutedText, Text } from "../../components/text";
@@ -17,20 +26,20 @@ interface LockOverlayProps {
17
26
  /** Optional success-toast message. Defaults to `${appName} unlocked`. */
18
27
  successMessage?: string;
19
28
  /**
20
- * Optional biometric/passkey unlock, shown as a prominent button above
21
- * the password field when provided. This overlay has no opinion on
22
- * *how* that happens (passkey PRF, platform secure-storage, etc.)
23
- * the caller owns the whole ceremony and is expected to swallow
24
- * expected failure modes (user cancel, nothing enrolled) rather than
25
- * throw; a throw here surfaces the same generic "unlock failed" error
26
- * the password path does, which is the wrong message for "you tapped
27
- * cancel on the Face ID prompt".
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.
28
41
  */
29
42
  onBiometricUnlock?: () => Promise<void> | void;
30
- /** Button label. Defaults to "Unlock with Biometrics" — pass
31
- * something more specific ("Unlock with Face ID") when the caller
32
- * can detect which authentication type is actually available. */
33
- biometricLabel?: string;
34
43
  }
35
44
 
36
45
  /**
@@ -45,22 +54,56 @@ export function LockOverlay({
45
54
  onSignOut,
46
55
  successMessage,
47
56
  onBiometricUnlock,
48
- biometricLabel,
49
57
  }: LockOverlayProps) {
50
58
  const { appName, Logo } = useAppConfig();
59
+ const { colorScheme } = useColorScheme();
60
+ const isDark = colorScheme === "dark";
51
61
  const [password, setPassword] = useState("");
52
62
  const [loading, setLoading] = useState(false);
53
- const [biometricLoading, setBiometricLoading] = useState(false);
63
+ const [biometricState, setBiometricState] = useState<"idle" | "prompting">("idle");
54
64
  const [error, setError] = useState("");
65
+ const pulse = useSharedValue(1);
55
66
 
56
- const handleBiometricUnlock = async () => {
57
- setBiometricLoading(true);
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
+ const triggerBiometricUnlock = React.useCallback(async () => {
81
+ if (!onBiometricUnlock) {
82
+ return;
83
+ }
84
+ setBiometricState("prompting");
58
85
  try {
59
- await onBiometricUnlock?.();
86
+ await onBiometricUnlock();
60
87
  } finally {
61
- setBiometricLoading(false);
88
+ setBiometricState("idle");
62
89
  }
63
- };
90
+ }, [onBiometricUnlock]);
91
+
92
+ useEffect(() => {
93
+ if (biometricState === "prompting") {
94
+ pulse.value = withRepeat(
95
+ withTiming(1.12, { duration: 650, easing: Easing.inOut(Easing.ease) }),
96
+ -1,
97
+ true
98
+ );
99
+ } else {
100
+ pulse.value = withTiming(1, { duration: 200 });
101
+ }
102
+ }, [biometricState, pulse]);
103
+
104
+ const pulseStyle = useAnimatedStyle(() => ({
105
+ transform: [{ scale: pulse.value }],
106
+ }));
64
107
 
65
108
  const handleUnlock = async () => {
66
109
  const captured = password;
@@ -110,26 +153,35 @@ export function LockOverlay({
110
153
  <MutedText className="mb-8 text-center text-sm">
111
154
  Enter your master password to unlock
112
155
  </MutedText>
113
- <View className="w-full gap-3">
114
- {onBiometricUnlock ? (
115
- <>
116
- <Button
117
- className="w-full"
118
- onPress={handleBiometricUnlock}
119
- isLoading={biometricLoading}
120
- accessibilityLabel={biometricLabel ?? "Unlock with Biometrics"}
121
- accessibilityRole="button"
122
- testID="lock-biometric-button"
156
+
157
+ {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"
123
169
  >
124
- {biometricLabel ?? "Unlock with Biometrics"}
125
- </Button>
126
- <View className="flex-row items-center gap-3 py-1">
127
- <View className="h-px flex-1 bg-border dark:bg-neutral-800" />
128
- <MutedText className="text-xs">or use your password</MutedText>
129
- <View className="h-px flex-1 bg-border dark:bg-neutral-800" />
130
- </View>
131
- </>
132
- ) : null}
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
+ </>
182
+ ) : null}
183
+
184
+ <View className="w-full gap-3">
133
185
  <PasswordInput
134
186
  value={password}
135
187
  onChangeText={(text) => {