@entropy-softworks/ui 2026.8.19 → 2026.8.21

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, 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".
21
+ */
22
+ 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;
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, biometricLabel, }: 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,qBA8FpF"}
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,5 +1,5 @@
1
1
  import React, { useState } from "react";
2
- import { View } from "react-native";
2
+ import { Platform, View } from "react-native";
3
3
  import Toast from "react-native-toast-message";
4
4
  import { Button } from "../../components/button";
5
5
  import { PasswordInput } from "../../components/input";
@@ -12,11 +12,21 @@ import { useAppConfig } from "../../context/AppConfigContext";
12
12
  * unlock work begins so the fiber tree no longer references the
13
13
  * plaintext during PBKDF2 / WebCrypto derivation.
14
14
  */
15
- export function LockOverlay({ onUnlock, onSignOut, successMessage }) {
15
+ export function LockOverlay({ onUnlock, onSignOut, successMessage, onBiometricUnlock, biometricLabel, }) {
16
16
  const { appName, Logo } = useAppConfig();
17
17
  const [password, setPassword] = useState("");
18
18
  const [loading, setLoading] = useState(false);
19
+ const [biometricLoading, setBiometricLoading] = useState(false);
19
20
  const [error, setError] = useState("");
21
+ const handleBiometricUnlock = async () => {
22
+ setBiometricLoading(true);
23
+ try {
24
+ await onBiometricUnlock?.();
25
+ }
26
+ finally {
27
+ setBiometricLoading(false);
28
+ }
29
+ };
20
30
  const handleUnlock = async () => {
21
31
  const captured = password;
22
32
  if (!captured.trim()) {
@@ -63,12 +73,30 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage }) {
63
73
  Enter your master password to unlock
64
74
  </MutedText>
65
75
  <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}
66
86
  <PasswordInput value={password} onChangeText={(text) => {
67
87
  setPassword(text);
68
88
  if (error) {
69
89
  setError("");
70
90
  }
71
- }} placeholder="Master password" error={error} autoFocus returnKeyType="done" onSubmitEditing={handleUnlock} accessibilityLabel="Master password" testID="lock-password-input"/>
91
+ }} placeholder="Master password" error={error}
92
+ // Web: autofocus on a login-style page is expected desktop
93
+ // UX. Native: this overlay mounts unconditionally at app
94
+ // launch (and can be visible under the OTA install overlay,
95
+ // before the user has done anything at all), so autofocus
96
+ // pops the on-screen keyboard immediately and unprompted —
97
+ // reported as a real annoyance on 2026-08-17. Unlock should
98
+ // wait for an explicit tap on the field.
99
+ autoFocus={Platform.OS === "web"} returnKeyType="done" onSubmitEditing={handleUnlock} accessibilityLabel="Master password" testID="lock-password-input"/>
72
100
  <Button className="w-full" onPress={handleUnlock} isLoading={loading} accessibilityLabel="Unlock" accessibilityRole="button" testID="lock-unlock-button">
73
101
  Unlock
74
102
  </Button>
@@ -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.19",
3
+ "version": "2026.8.21",
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,5 +1,5 @@
1
1
  import React, { useState } from "react";
2
- import { View } from "react-native";
2
+ import { Platform, View } from "react-native";
3
3
  import Toast from "react-native-toast-message";
4
4
  import { Button } from "../../components/button";
5
5
  import { PasswordInput } from "../../components/input";
@@ -16,6 +16,21 @@ interface LockOverlayProps {
16
16
  onSignOut: () => void;
17
17
  /** Optional success-toast message. Defaults to `${appName} unlocked`. */
18
18
  successMessage?: string;
19
+ /**
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".
28
+ */
29
+ 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;
19
34
  }
20
35
 
21
36
  /**
@@ -25,12 +40,28 @@ interface LockOverlayProps {
25
40
  * unlock work begins so the fiber tree no longer references the
26
41
  * plaintext during PBKDF2 / WebCrypto derivation.
27
42
  */
28
- export function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlayProps) {
43
+ export function LockOverlay({
44
+ onUnlock,
45
+ onSignOut,
46
+ successMessage,
47
+ onBiometricUnlock,
48
+ biometricLabel,
49
+ }: LockOverlayProps) {
29
50
  const { appName, Logo } = useAppConfig();
30
51
  const [password, setPassword] = useState("");
31
52
  const [loading, setLoading] = useState(false);
53
+ const [biometricLoading, setBiometricLoading] = useState(false);
32
54
  const [error, setError] = useState("");
33
55
 
56
+ const handleBiometricUnlock = async () => {
57
+ setBiometricLoading(true);
58
+ try {
59
+ await onBiometricUnlock?.();
60
+ } finally {
61
+ setBiometricLoading(false);
62
+ }
63
+ };
64
+
34
65
  const handleUnlock = async () => {
35
66
  const captured = password;
36
67
  if (!captured.trim()) {
@@ -80,6 +111,25 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlay
80
111
  Enter your master password to unlock
81
112
  </MutedText>
82
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"
123
+ >
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}
83
133
  <PasswordInput
84
134
  value={password}
85
135
  onChangeText={(text) => {
@@ -90,7 +140,14 @@ export function LockOverlay({ onUnlock, onSignOut, successMessage }: LockOverlay
90
140
  }}
91
141
  placeholder="Master password"
92
142
  error={error}
93
- autoFocus
143
+ // Web: autofocus on a login-style page is expected desktop
144
+ // UX. Native: this overlay mounts unconditionally at app
145
+ // launch (and can be visible under the OTA install overlay,
146
+ // before the user has done anything at all), so autofocus
147
+ // pops the on-screen keyboard immediately and unprompted —
148
+ // reported as a real annoyance on 2026-08-17. Unlock should
149
+ // wait for an explicit tap on the field.
150
+ autoFocus={Platform.OS === "web"}
94
151
  returnKeyType="done"
95
152
  onSubmitEditing={handleUnlock}
96
153
  accessibilityLabel="Master password"