@entropy-softworks/ui 2026.8.21 → 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.
|
@@ -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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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,
|
|
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,
|
|
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,21 +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, onBiometricUnlock,
|
|
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 [
|
|
24
|
+
const [biometricState, setBiometricState] = useState("idle");
|
|
20
25
|
const [error, setError] = useState("");
|
|
21
|
-
const
|
|
22
|
-
|
|
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");
|
|
23
33
|
try {
|
|
24
|
-
await onBiometricUnlock
|
|
34
|
+
await onBiometricUnlock();
|
|
25
35
|
}
|
|
26
36
|
finally {
|
|
27
|
-
|
|
37
|
+
setBiometricState("idle");
|
|
28
38
|
}
|
|
29
|
-
};
|
|
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
|
+
}));
|
|
30
62
|
const handleUnlock = async () => {
|
|
31
63
|
const captured = password;
|
|
32
64
|
if (!captured.trim()) {
|
|
@@ -72,17 +104,24 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUn
|
|
|
72
104
|
<MutedText className="mb-8 text-center text-sm">
|
|
73
105
|
Enter your master password to unlock
|
|
74
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
|
+
|
|
75
124
|
<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
125
|
<PasswordInput value={password} onChangeText={(text) => {
|
|
87
126
|
setPassword(text);
|
|
88
127
|
if (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LockOverlay.jsx","sourceRoot":"","sources":["../../../src/components/auth/LockOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
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,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";
|
|
@@ -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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 [
|
|
63
|
+
const [biometricState, setBiometricState] = useState<"idle" | "prompting">("idle");
|
|
54
64
|
const [error, setError] = useState("");
|
|
65
|
+
const hasAutoPrompted = useRef(false);
|
|
66
|
+
const pulse = useSharedValue(1);
|
|
55
67
|
|
|
56
|
-
const
|
|
57
|
-
|
|
68
|
+
const triggerBiometricUnlock = React.useCallback(async () => {
|
|
69
|
+
if (!onBiometricUnlock) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setBiometricState("prompting");
|
|
58
73
|
try {
|
|
59
|
-
await onBiometricUnlock
|
|
74
|
+
await onBiometricUnlock();
|
|
60
75
|
} finally {
|
|
61
|
-
|
|
76
|
+
setBiometricState("idle");
|
|
62
77
|
}
|
|
63
|
-
};
|
|
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
|
+
}));
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
{
|
|
125
|
-
</
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
|
|
184
|
+
<View className="w-full gap-3">
|
|
133
185
|
<PasswordInput
|
|
134
186
|
value={password}
|
|
135
187
|
onChangeText={(text) => {
|