@entropy-softworks/ui 2026.8.17 → 2026.8.19
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.
- package/lib/hooks/useAutoLock.d.ts +22 -1
- package/lib/hooks/useAutoLock.d.ts.map +1 -1
- package/lib/hooks/useAutoLock.js +7 -1
- package/lib/hooks/useAutoLock.js.map +1 -1
- package/lib/utils/fetch.d.ts.map +1 -1
- package/lib/utils/fetch.js +37 -1
- package/lib/utils/fetch.js.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useAutoLock.ts +32 -2
- package/src/utils/fetch.ts +38 -1
|
@@ -12,11 +12,32 @@ export interface UseAutoLockOptions {
|
|
|
12
12
|
/** Idle threshold in milliseconds. Defaults to 10 minutes. */
|
|
13
13
|
idleLockMs?: number;
|
|
14
14
|
}
|
|
15
|
+
export interface UseAutoLockResult {
|
|
16
|
+
/**
|
|
17
|
+
* Call on any real user interaction to reset the idle clock. Required
|
|
18
|
+
* on native: unlike web (which listens on `window` for
|
|
19
|
+
* mousemove/touchstart/scroll/keydown directly), RN has no global
|
|
20
|
+
* "any touch happened anywhere" event — a touch inside a `Pressable`,
|
|
21
|
+
* `ScrollView`, etc. never reaches `window`, because there is no
|
|
22
|
+
* `window` to reach. Without a caller wiring this up (e.g. a capture-
|
|
23
|
+
* phase `onStartShouldSetResponderCapture` at the app root, returning
|
|
24
|
+
* `false` so it never actually claims the touch), the idle timer is
|
|
25
|
+
* armed once at mount and never reset — the session locks exactly
|
|
26
|
+
* `idleLockMs` after mount/foreground REGARDLESS of how actively the
|
|
27
|
+
* app is being used. Confirmed for real: a vault mid-active-use lost
|
|
28
|
+
* its key mid-session, and every item silently failed to decrypt
|
|
29
|
+
* (`Promise.allSettled` in the caller's list-fetch treats a decrypt
|
|
30
|
+
* failure as "drop this item", not "the vault relocked"), which reads
|
|
31
|
+
* to the user as vault items simply vanishing — no error, no visible
|
|
32
|
+
* cause, on 2026-08-17.
|
|
33
|
+
*/
|
|
34
|
+
notifyActivity: () => void;
|
|
35
|
+
}
|
|
15
36
|
/**
|
|
16
37
|
* Auto-lock a session after a period of inactivity. Generic over the
|
|
17
38
|
* lock predicate — vault wires `KeyManager.isUnlocked` / `KeyManager.lock`,
|
|
18
39
|
* other apps wire their own state. Web uses an idle-timer only; native
|
|
19
40
|
* additionally locks immediately when the app goes to the background.
|
|
20
41
|
*/
|
|
21
|
-
export declare function useAutoLock(options: UseAutoLockOptions):
|
|
42
|
+
export declare function useAutoLock(options: UseAutoLockOptions): UseAutoLockResult;
|
|
22
43
|
//# sourceMappingURL=useAutoLock.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoLock.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,
|
|
1
|
+
{"version":3,"file":"useAutoLock.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,CA2F1E"}
|
package/lib/hooks/useAutoLock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { AppState as RNAppState, Platform } from "react-native";
|
|
3
3
|
const DEFAULT_IDLE_LOCK_MS = 10 * 60 * 1000;
|
|
4
4
|
/**
|
|
@@ -9,6 +9,7 @@ const DEFAULT_IDLE_LOCK_MS = 10 * 60 * 1000;
|
|
|
9
9
|
*/
|
|
10
10
|
export function useAutoLock(options) {
|
|
11
11
|
const { isUnlocked, onLock, isEnabled, idleLockMs = DEFAULT_IDLE_LOCK_MS } = options;
|
|
12
|
+
const onActivityRef = useRef(() => { });
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
let idleTimer = null;
|
|
14
15
|
let lastActivityAt = Date.now();
|
|
@@ -43,6 +44,7 @@ export function useAutoLock(options) {
|
|
|
43
44
|
lastActivityAt = Date.now();
|
|
44
45
|
armIdleTimer();
|
|
45
46
|
};
|
|
47
|
+
onActivityRef.current = onActivity;
|
|
46
48
|
// On web, RN Web's AppState mirrors document visibility — tab-hidden
|
|
47
49
|
// becomes 'background', which would otherwise fire an immediate lock.
|
|
48
50
|
// Only treat 'background' as a lock signal on real native platforms.
|
|
@@ -81,5 +83,9 @@ export function useAutoLock(options) {
|
|
|
81
83
|
};
|
|
82
84
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
85
|
}, []);
|
|
86
|
+
const notifyActivity = useCallback(() => {
|
|
87
|
+
onActivityRef.current();
|
|
88
|
+
}, []);
|
|
89
|
+
return { notifyActivity };
|
|
84
90
|
}
|
|
85
91
|
//# sourceMappingURL=useAutoLock.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoLock.js","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useAutoLock.js","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"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"}
|
package/lib/utils/fetch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/utils/fetch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/utils/fetch.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,WAAgB,EACzB,SAAS,SAAQ,GAChB,OAAO,CAAC,QAAQ,CAAC,CAanB"}
|
package/lib/utils/fetch.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combines multiple AbortSignals into one that aborts when any input does.
|
|
3
|
+
* Prefers the native `AbortSignal.any` where present (modern browsers);
|
|
4
|
+
* falls back to manual event-listener wiring where it isn't.
|
|
5
|
+
*
|
|
6
|
+
* That fallback is load-bearing, not defensive padding: React Native
|
|
7
|
+
* doesn't provide a native AbortController at all — `setUpXHR.js` installs
|
|
8
|
+
* one from the third-party `abort-controller` npm package (pinned at
|
|
9
|
+
* v3.0.0 in this tree), which predates `AbortSignal.any` entering the
|
|
10
|
+
* WHATWG spec. Calling it unconditionally on native evaluates to invoking
|
|
11
|
+
* `undefined` as a function, which Hermes reports as the unhelpfully
|
|
12
|
+
* generic "undefined is not a function" — confirmed for real on
|
|
13
|
+
* 2026-08-17, the first time a native call site (a vault item detail
|
|
14
|
+
* screen, via its own cancellation AbortSignal) exercised the
|
|
15
|
+
* `signals.length > 1` branch this function has always had; every
|
|
16
|
+
* previous native caller only ever supplied the internal timeout signal,
|
|
17
|
+
* so `AbortSignal.any` had simply never been reached before.
|
|
18
|
+
*/
|
|
19
|
+
function combineSignals(signals) {
|
|
20
|
+
if (signals.length === 1) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked
|
|
22
|
+
return signals[0];
|
|
23
|
+
}
|
|
24
|
+
if (typeof AbortSignal.any === "function") {
|
|
25
|
+
return AbortSignal.any([...signals]);
|
|
26
|
+
}
|
|
27
|
+
const combined = new AbortController();
|
|
28
|
+
for (const s of signals) {
|
|
29
|
+
if (s.aborted) {
|
|
30
|
+
combined.abort();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
s.addEventListener("abort", () => combined.abort(), { once: true });
|
|
34
|
+
}
|
|
35
|
+
return combined.signal;
|
|
36
|
+
}
|
|
1
37
|
/**
|
|
2
38
|
* `fetch` with a timeout abort signal merged onto the caller's own signal.
|
|
3
39
|
* The default timeout is 15s. Pass `Infinity` to disable the timeout
|
|
@@ -19,7 +55,7 @@ export function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
|
|
|
19
55
|
? setTimeout(() => controller.abort(), timeoutMs)
|
|
20
56
|
: null;
|
|
21
57
|
const signals = [controller.signal, options.signal].filter((s) => s != null);
|
|
22
|
-
const combinedSignal =
|
|
58
|
+
const combinedSignal = combineSignals(signals);
|
|
23
59
|
return fetch(url, { ...options, signal: combinedSignal }).finally(() => {
|
|
24
60
|
if (id !== null) {
|
|
25
61
|
clearTimeout(id);
|
package/lib/utils/fetch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/utils/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAW,EACX,UAAuB,EAAE,EACzB,SAAS,GAAG,KAAK;IAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,EAAE,GACN,SAAS,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QACzC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC;QACjD,CAAC,CAAC,IAAI,CAAC;IACX,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IAC/F,MAAM,cAAc,GAAG,
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/utils/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,cAAc,CAAC,OAA+B;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,sFAAsF;QACtF,OAAO,OAAO,CAAC,CAAC,CAAE,CAAC;IACrB,CAAC;IACD,IAAI,OAAO,WAAW,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QAC1C,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YACd,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM;QACR,CAAC;QACD,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAW,EACX,UAAuB,EAAE,EACzB,SAAS,GAAG,KAAK;IAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,EAAE,GACN,SAAS,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QACzC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC;QACjD,CAAC,CAAC,IAAI,CAAC;IACX,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IAC/F,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;QACrE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,YAAY,CAAC,EAAE,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
package/src/hooks/useAutoLock.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { AppState as RNAppState, Platform } from "react-native";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_IDLE_LOCK_MS = 10 * 60 * 1000;
|
|
@@ -18,14 +18,37 @@ export interface UseAutoLockOptions {
|
|
|
18
18
|
idleLockMs?: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface UseAutoLockResult {
|
|
22
|
+
/**
|
|
23
|
+
* Call on any real user interaction to reset the idle clock. Required
|
|
24
|
+
* on native: unlike web (which listens on `window` for
|
|
25
|
+
* mousemove/touchstart/scroll/keydown directly), RN has no global
|
|
26
|
+
* "any touch happened anywhere" event — a touch inside a `Pressable`,
|
|
27
|
+
* `ScrollView`, etc. never reaches `window`, because there is no
|
|
28
|
+
* `window` to reach. Without a caller wiring this up (e.g. a capture-
|
|
29
|
+
* phase `onStartShouldSetResponderCapture` at the app root, returning
|
|
30
|
+
* `false` so it never actually claims the touch), the idle timer is
|
|
31
|
+
* armed once at mount and never reset — the session locks exactly
|
|
32
|
+
* `idleLockMs` after mount/foreground REGARDLESS of how actively the
|
|
33
|
+
* app is being used. Confirmed for real: a vault mid-active-use lost
|
|
34
|
+
* its key mid-session, and every item silently failed to decrypt
|
|
35
|
+
* (`Promise.allSettled` in the caller's list-fetch treats a decrypt
|
|
36
|
+
* failure as "drop this item", not "the vault relocked"), which reads
|
|
37
|
+
* to the user as vault items simply vanishing — no error, no visible
|
|
38
|
+
* cause, on 2026-08-17.
|
|
39
|
+
*/
|
|
40
|
+
notifyActivity: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
/**
|
|
22
44
|
* Auto-lock a session after a period of inactivity. Generic over the
|
|
23
45
|
* lock predicate — vault wires `KeyManager.isUnlocked` / `KeyManager.lock`,
|
|
24
46
|
* other apps wire their own state. Web uses an idle-timer only; native
|
|
25
47
|
* additionally locks immediately when the app goes to the background.
|
|
26
48
|
*/
|
|
27
|
-
export function useAutoLock(options: UseAutoLockOptions):
|
|
49
|
+
export function useAutoLock(options: UseAutoLockOptions): UseAutoLockResult {
|
|
28
50
|
const { isUnlocked, onLock, isEnabled, idleLockMs = DEFAULT_IDLE_LOCK_MS } = options;
|
|
51
|
+
const onActivityRef = useRef<() => void>(() => {});
|
|
29
52
|
|
|
30
53
|
useEffect(() => {
|
|
31
54
|
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -64,6 +87,7 @@ export function useAutoLock(options: UseAutoLockOptions): void {
|
|
|
64
87
|
lastActivityAt = Date.now();
|
|
65
88
|
armIdleTimer();
|
|
66
89
|
};
|
|
90
|
+
onActivityRef.current = onActivity;
|
|
67
91
|
|
|
68
92
|
// On web, RN Web's AppState mirrors document visibility — tab-hidden
|
|
69
93
|
// becomes 'background', which would otherwise fire an immediate lock.
|
|
@@ -107,4 +131,10 @@ export function useAutoLock(options: UseAutoLockOptions): void {
|
|
|
107
131
|
};
|
|
108
132
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
133
|
}, []);
|
|
134
|
+
|
|
135
|
+
const notifyActivity = useCallback(() => {
|
|
136
|
+
onActivityRef.current();
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
return { notifyActivity };
|
|
110
140
|
}
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combines multiple AbortSignals into one that aborts when any input does.
|
|
3
|
+
* Prefers the native `AbortSignal.any` where present (modern browsers);
|
|
4
|
+
* falls back to manual event-listener wiring where it isn't.
|
|
5
|
+
*
|
|
6
|
+
* That fallback is load-bearing, not defensive padding: React Native
|
|
7
|
+
* doesn't provide a native AbortController at all — `setUpXHR.js` installs
|
|
8
|
+
* one from the third-party `abort-controller` npm package (pinned at
|
|
9
|
+
* v3.0.0 in this tree), which predates `AbortSignal.any` entering the
|
|
10
|
+
* WHATWG spec. Calling it unconditionally on native evaluates to invoking
|
|
11
|
+
* `undefined` as a function, which Hermes reports as the unhelpfully
|
|
12
|
+
* generic "undefined is not a function" — confirmed for real on
|
|
13
|
+
* 2026-08-17, the first time a native call site (a vault item detail
|
|
14
|
+
* screen, via its own cancellation AbortSignal) exercised the
|
|
15
|
+
* `signals.length > 1` branch this function has always had; every
|
|
16
|
+
* previous native caller only ever supplied the internal timeout signal,
|
|
17
|
+
* so `AbortSignal.any` had simply never been reached before.
|
|
18
|
+
*/
|
|
19
|
+
function combineSignals(signals: readonly AbortSignal[]): AbortSignal {
|
|
20
|
+
if (signals.length === 1) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked
|
|
22
|
+
return signals[0]!;
|
|
23
|
+
}
|
|
24
|
+
if (typeof AbortSignal.any === "function") {
|
|
25
|
+
return AbortSignal.any([...signals]);
|
|
26
|
+
}
|
|
27
|
+
const combined = new AbortController();
|
|
28
|
+
for (const s of signals) {
|
|
29
|
+
if (s.aborted) {
|
|
30
|
+
combined.abort();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
s.addEventListener("abort", () => combined.abort(), { once: true });
|
|
34
|
+
}
|
|
35
|
+
return combined.signal;
|
|
36
|
+
}
|
|
37
|
+
|
|
1
38
|
/**
|
|
2
39
|
* `fetch` with a timeout abort signal merged onto the caller's own signal.
|
|
3
40
|
* The default timeout is 15s. Pass `Infinity` to disable the timeout
|
|
@@ -24,7 +61,7 @@ export function fetchWithTimeout(
|
|
|
24
61
|
? setTimeout(() => controller.abort(), timeoutMs)
|
|
25
62
|
: null;
|
|
26
63
|
const signals = [controller.signal, options.signal].filter((s): s is AbortSignal => s != null);
|
|
27
|
-
const combinedSignal =
|
|
64
|
+
const combinedSignal = combineSignals(signals);
|
|
28
65
|
return fetch(url, { ...options, signal: combinedSignal }).finally(() => {
|
|
29
66
|
if (id !== null) {
|
|
30
67
|
clearTimeout(id);
|