@entropy-softworks/ui 2026.8.20 → 2026.8.22

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.
@@ -9,6 +9,21 @@ interface LockOverlayProps {
9
9
  onSignOut: () => void;
10
10
  /** Optional success-toast message. Defaults to `${appName} unlocked`. */
11
11
  successMessage?: string;
12
+ /**
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.
25
+ */
26
+ onBiometricUnlock?: () => Promise<void> | void;
12
27
  }
13
28
  /**
14
29
  * Unlock form for session-locked flows (e.g. vault re-lock after idle).
@@ -17,6 +32,6 @@ interface LockOverlayProps {
17
32
  * unlock work begins so the fiber tree no longer references the
18
33
  * plaintext during PBKDF2 / WebCrypto derivation.
19
34
  */
20
- export declare function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlayProps): React.JSX.Element;
35
+ export declare function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, }: LockOverlayProps): React.JSX.Element;
21
36
  export {};
22
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;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE,gBAAgB,qBAqGpF"}
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;;;;;;;;;;;;;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, useRef, 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,11 +15,50 @@ 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 }) {
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);
24
+ const [biometricState, setBiometricState] = useState("idle");
19
25
  const [error, setError] = useState("");
26
+ const hasAutoPrompted = useRef(false);
27
+ const pulse = useSharedValue(1);
28
+ const triggerBiometricUnlock = React.useCallback(async () => {
29
+ if (!onBiometricUnlock) {
30
+ return;
31
+ }
32
+ setBiometricState("prompting");
33
+ try {
34
+ await onBiometricUnlock();
35
+ }
36
+ finally {
37
+ setBiometricState("idle");
38
+ }
39
+ }, [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
+ useEffect(() => {
46
+ if (onBiometricUnlock && !hasAutoPrompted.current) {
47
+ hasAutoPrompted.current = true;
48
+ void triggerBiometricUnlock();
49
+ }
50
+ }, [onBiometricUnlock, triggerBiometricUnlock]);
51
+ useEffect(() => {
52
+ if (biometricState === "prompting") {
53
+ pulse.value = withRepeat(withTiming(1.12, { duration: 650, easing: Easing.inOut(Easing.ease) }), -1, true);
54
+ }
55
+ else {
56
+ pulse.value = withTiming(1, { duration: 200 });
57
+ }
58
+ }, [biometricState, pulse]);
59
+ const pulseStyle = useAnimatedStyle(() => ({
60
+ transform: [{ scale: pulse.value }],
61
+ }));
20
62
  const handleUnlock = async () => {
21
63
  const captured = password;
22
64
  if (!captured.trim()) {
@@ -62,6 +104,23 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage }) {
62
104
  <MutedText className="mb-8 text-center text-sm">
63
105
  Enter your master password to unlock
64
106
  </MutedText>
107
+
108
+ {onBiometricUnlock ? (<>
109
+ <Pressable onPress={triggerBiometricUnlock} accessibilityRole="button" accessibilityLabel="Unlock with biometrics" testID="lock-biometric-icon" className="mb-3 items-center">
110
+ <Animated.View style={pulseStyle} className="h-20 w-20 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-900">
111
+ <Ionicons name="finger-print" size={44} color={isDark ? "#FFFFFF" : "#000000"}/>
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}
123
+
65
124
  <View className="w-full gap-3">
66
125
  <PasswordInput value={password} onChangeText={(text) => {
67
126
  setPassword(text);
@@ -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.20",
3
+ "version": "2026.8.22",
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, useRef, 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";
@@ -16,6 +25,21 @@ interface LockOverlayProps {
16
25
  onSignOut: () => void;
17
26
  /** Optional success-toast message. Defaults to `${appName} unlocked`. */
18
27
  successMessage?: string;
28
+ /**
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.
41
+ */
42
+ onBiometricUnlock?: () => Promise<void> | void;
19
43
  }
20
44
 
21
45
  /**
@@ -25,11 +49,61 @@ interface LockOverlayProps {
25
49
  * unlock work begins so the fiber tree no longer references the
26
50
  * plaintext during PBKDF2 / WebCrypto derivation.
27
51
  */
28
- export function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlayProps) {
52
+ export function LockOverlay({
53
+ onUnlock,
54
+ onSignOut,
55
+ successMessage,
56
+ onBiometricUnlock,
57
+ }: LockOverlayProps) {
29
58
  const { appName, Logo } = useAppConfig();
59
+ const { colorScheme } = useColorScheme();
60
+ const isDark = colorScheme === "dark";
30
61
  const [password, setPassword] = useState("");
31
62
  const [loading, setLoading] = useState(false);
63
+ const [biometricState, setBiometricState] = useState<"idle" | "prompting">("idle");
32
64
  const [error, setError] = useState("");
65
+ const hasAutoPrompted = useRef(false);
66
+ const pulse = useSharedValue(1);
67
+
68
+ const triggerBiometricUnlock = React.useCallback(async () => {
69
+ if (!onBiometricUnlock) {
70
+ return;
71
+ }
72
+ setBiometricState("prompting");
73
+ try {
74
+ await onBiometricUnlock();
75
+ } finally {
76
+ setBiometricState("idle");
77
+ }
78
+ }, [onBiometricUnlock]);
79
+
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
+ useEffect(() => {
86
+ if (onBiometricUnlock && !hasAutoPrompted.current) {
87
+ hasAutoPrompted.current = true;
88
+ void triggerBiometricUnlock();
89
+ }
90
+ }, [onBiometricUnlock, triggerBiometricUnlock]);
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
+ }));
33
107
 
34
108
  const handleUnlock = async () => {
35
109
  const captured = password;
@@ -79,6 +153,34 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlay
79
153
  <MutedText className="mb-8 text-center text-sm">
80
154
  Enter your master password to unlock
81
155
  </MutedText>
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"
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 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
+ </>
182
+ ) : null}
183
+
82
184
  <View className="w-full gap-3">
83
185
  <PasswordInput
84
186
  value={password}