@entropy-softworks/ui 2026.9.4 → 2026.9.6
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoLock.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useAutoLock.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"AAUA,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,CAqJ1E"}
|
package/lib/hooks/useAutoLock.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
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
|
+
/** How often the suspend detector below checks in. */
|
|
5
|
+
const HEARTBEAT_MS = 1000;
|
|
6
|
+
/** A gap this large between checks means the app was not running. */
|
|
7
|
+
const SUSPEND_GAP_MS = 4000;
|
|
4
8
|
/**
|
|
5
9
|
* Auto-lock a session after a period of inactivity. Generic over the
|
|
6
10
|
* lock predicate — vault wires `KeyManager.isUnlocked` / `KeyManager.lock`,
|
|
@@ -45,17 +49,72 @@ export function useAutoLock(options) {
|
|
|
45
49
|
armIdleTimer();
|
|
46
50
|
};
|
|
47
51
|
onActivityRef.current = onActivity;
|
|
52
|
+
/**
|
|
53
|
+
* Leaving the app, by any of the routes the OS calls leaving.
|
|
54
|
+
*
|
|
55
|
+
* 'background' alone was not enough. Locking the phone with the app open
|
|
56
|
+
* and then unlocking the phone came back to an unlocked vault, because the
|
|
57
|
+
* event that arrives for a screen-off is not reliably 'background' — RN
|
|
58
|
+
* documents 'inactive' as iOS-only, and what a given Android version emits
|
|
59
|
+
* when the keyguard comes up is not something to bet a password manager on.
|
|
60
|
+
*
|
|
61
|
+
* So both count, and — more importantly — the return is checked too. See
|
|
62
|
+
* below: 'active' always arrives, whatever was or was not emitted on the
|
|
63
|
+
* way out, which makes it the one signal that cannot be missed.
|
|
64
|
+
*/
|
|
65
|
+
let awaySince = null;
|
|
48
66
|
// On web, RN Web's AppState mirrors document visibility — tab-hidden
|
|
49
67
|
// becomes 'background', which would otherwise fire an immediate lock.
|
|
50
|
-
// Only treat
|
|
68
|
+
// Only treat leaving as a lock signal on real native platforms.
|
|
51
69
|
const subscription = RNAppState.addEventListener("change", (nextState) => {
|
|
52
|
-
|
|
70
|
+
const native = Platform.OS !== "web";
|
|
71
|
+
if (native && (nextState === "background" || nextState === "inactive")) {
|
|
72
|
+
if (awaySince === null) {
|
|
73
|
+
awaySince = Date.now();
|
|
74
|
+
}
|
|
53
75
|
lockIfEnabled();
|
|
54
76
|
}
|
|
55
77
|
else if (nextState === "active") {
|
|
78
|
+
// The backstop. If the departure was missed, or was suppressed by an
|
|
79
|
+
// app-initiated system prompt that has since ended, this is where it
|
|
80
|
+
// gets caught — the vault must never be found open after the app was
|
|
81
|
+
// away, and coming back is the last moment to make that true.
|
|
82
|
+
const wasAway = awaySince !== null;
|
|
83
|
+
awaySince = null;
|
|
84
|
+
if (native && wasAway) {
|
|
85
|
+
lockIfEnabled();
|
|
86
|
+
}
|
|
56
87
|
armIdleTimer();
|
|
57
88
|
}
|
|
58
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* The backstop that does not involve AppState at all.
|
|
92
|
+
*
|
|
93
|
+
* AppState was the only way this hook knew the app had gone away, and on a
|
|
94
|
+
* real device it was reported not firing for either a minimise or a screen
|
|
95
|
+
* lock — the vault was still open on return, with the setting on. Rather
|
|
96
|
+
* than keep guessing which event a given platform version emits, this
|
|
97
|
+
* measures the thing that actually matters: a suspended app's timers stop.
|
|
98
|
+
*
|
|
99
|
+
* The interval ticks every second. If more than `SUSPEND_GAP_MS` of wall
|
|
100
|
+
* clock passed since the last tick, the app was not running, whatever the
|
|
101
|
+
* OS did or did not tell us — so lock.
|
|
102
|
+
*
|
|
103
|
+
* The threshold is well above any plausible stall on the JS thread, which
|
|
104
|
+
* is the one other thing that can delay an interval. Four seconds is far
|
|
105
|
+
* more than the heaviest work this app does in one go (opening a vault is
|
|
106
|
+
* milliseconds now that the crypto is native) and still short enough that
|
|
107
|
+
* putting a phone down and picking it up again lands on a locked vault.
|
|
108
|
+
*/
|
|
109
|
+
let lastTick = Date.now();
|
|
110
|
+
const heartbeat = setInterval(() => {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const gap = now - lastTick;
|
|
113
|
+
lastTick = now;
|
|
114
|
+
if (Platform.OS !== "web" && gap > SUSPEND_GAP_MS) {
|
|
115
|
+
lockIfEnabled();
|
|
116
|
+
}
|
|
117
|
+
}, HEARTBEAT_MS);
|
|
59
118
|
const cleanups = [];
|
|
60
119
|
if (Platform.OS === "web" && typeof document !== "undefined") {
|
|
61
120
|
const onVisibility = () => {
|
|
@@ -74,6 +133,7 @@ export function useAutoLock(options) {
|
|
|
74
133
|
armIdleTimer();
|
|
75
134
|
return () => {
|
|
76
135
|
subscription.remove();
|
|
136
|
+
clearInterval(heartbeat);
|
|
77
137
|
for (const fn of cleanups) {
|
|
78
138
|
fn();
|
|
79
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoLock.js","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEhE,MAAM,oBAAoB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"useAutoLock.js","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/hooks/useAutoLock.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { AppState as RNAppState, Platform } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_IDLE_LOCK_MS = 10 * 60 * 1000;
|
|
5
5
|
|
|
6
|
+
/** How often the suspend detector below checks in. */
|
|
7
|
+
const HEARTBEAT_MS = 1000;
|
|
8
|
+
/** A gap this large between checks means the app was not running. */
|
|
9
|
+
const SUSPEND_GAP_MS = 4000;
|
|
10
|
+
|
|
6
11
|
export interface UseAutoLockOptions {
|
|
7
12
|
/**
|
|
8
13
|
* Reports whether the session is currently "unlocked" (vault-style
|
|
@@ -89,17 +94,74 @@ export function useAutoLock(options: UseAutoLockOptions): UseAutoLockResult {
|
|
|
89
94
|
};
|
|
90
95
|
onActivityRef.current = onActivity;
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Leaving the app, by any of the routes the OS calls leaving.
|
|
99
|
+
*
|
|
100
|
+
* 'background' alone was not enough. Locking the phone with the app open
|
|
101
|
+
* and then unlocking the phone came back to an unlocked vault, because the
|
|
102
|
+
* event that arrives for a screen-off is not reliably 'background' — RN
|
|
103
|
+
* documents 'inactive' as iOS-only, and what a given Android version emits
|
|
104
|
+
* when the keyguard comes up is not something to bet a password manager on.
|
|
105
|
+
*
|
|
106
|
+
* So both count, and — more importantly — the return is checked too. See
|
|
107
|
+
* below: 'active' always arrives, whatever was or was not emitted on the
|
|
108
|
+
* way out, which makes it the one signal that cannot be missed.
|
|
109
|
+
*/
|
|
110
|
+
let awaySince: number | null = null;
|
|
111
|
+
|
|
92
112
|
// On web, RN Web's AppState mirrors document visibility — tab-hidden
|
|
93
113
|
// becomes 'background', which would otherwise fire an immediate lock.
|
|
94
|
-
// Only treat
|
|
114
|
+
// Only treat leaving as a lock signal on real native platforms.
|
|
95
115
|
const subscription = RNAppState.addEventListener("change", (nextState) => {
|
|
96
|
-
|
|
116
|
+
const native = Platform.OS !== "web";
|
|
117
|
+
if (native && (nextState === "background" || nextState === "inactive")) {
|
|
118
|
+
if (awaySince === null) {
|
|
119
|
+
awaySince = Date.now();
|
|
120
|
+
}
|
|
97
121
|
lockIfEnabled();
|
|
98
122
|
} else if (nextState === "active") {
|
|
123
|
+
// The backstop. If the departure was missed, or was suppressed by an
|
|
124
|
+
// app-initiated system prompt that has since ended, this is where it
|
|
125
|
+
// gets caught — the vault must never be found open after the app was
|
|
126
|
+
// away, and coming back is the last moment to make that true.
|
|
127
|
+
const wasAway = awaySince !== null;
|
|
128
|
+
awaySince = null;
|
|
129
|
+
if (native && wasAway) {
|
|
130
|
+
lockIfEnabled();
|
|
131
|
+
}
|
|
99
132
|
armIdleTimer();
|
|
100
133
|
}
|
|
101
134
|
});
|
|
102
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The backstop that does not involve AppState at all.
|
|
138
|
+
*
|
|
139
|
+
* AppState was the only way this hook knew the app had gone away, and on a
|
|
140
|
+
* real device it was reported not firing for either a minimise or a screen
|
|
141
|
+
* lock — the vault was still open on return, with the setting on. Rather
|
|
142
|
+
* than keep guessing which event a given platform version emits, this
|
|
143
|
+
* measures the thing that actually matters: a suspended app's timers stop.
|
|
144
|
+
*
|
|
145
|
+
* The interval ticks every second. If more than `SUSPEND_GAP_MS` of wall
|
|
146
|
+
* clock passed since the last tick, the app was not running, whatever the
|
|
147
|
+
* OS did or did not tell us — so lock.
|
|
148
|
+
*
|
|
149
|
+
* The threshold is well above any plausible stall on the JS thread, which
|
|
150
|
+
* is the one other thing that can delay an interval. Four seconds is far
|
|
151
|
+
* more than the heaviest work this app does in one go (opening a vault is
|
|
152
|
+
* milliseconds now that the crypto is native) and still short enough that
|
|
153
|
+
* putting a phone down and picking it up again lands on a locked vault.
|
|
154
|
+
*/
|
|
155
|
+
let lastTick = Date.now();
|
|
156
|
+
const heartbeat = setInterval(() => {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const gap = now - lastTick;
|
|
159
|
+
lastTick = now;
|
|
160
|
+
if (Platform.OS !== "web" && gap > SUSPEND_GAP_MS) {
|
|
161
|
+
lockIfEnabled();
|
|
162
|
+
}
|
|
163
|
+
}, HEARTBEAT_MS);
|
|
164
|
+
|
|
103
165
|
const cleanups: (() => void)[] = [];
|
|
104
166
|
|
|
105
167
|
if (Platform.OS === "web" && typeof document !== "undefined") {
|
|
@@ -122,6 +184,7 @@ export function useAutoLock(options: UseAutoLockOptions): UseAutoLockResult {
|
|
|
122
184
|
|
|
123
185
|
return () => {
|
|
124
186
|
subscription.remove();
|
|
187
|
+
clearInterval(heartbeat);
|
|
125
188
|
for (const fn of cleanups) {
|
|
126
189
|
fn();
|
|
127
190
|
}
|