@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":"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"}
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"}
@@ -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 'background' as a lock signal on real native platforms.
68
+ // Only treat leaving as a lock signal on real native platforms.
51
69
  const subscription = RNAppState.addEventListener("change", (nextState) => {
52
- if (nextState === "background" && Platform.OS !== "web") {
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":"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"}
1
+ {"version":3,"file":"useAutoLock.js","sourceRoot":"","sources":["../../src/hooks/useAutoLock.ts"],"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.9.4",
3
+ "version": "2026.9.6",
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",
@@ -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 'background' as a lock signal on real native platforms.
114
+ // Only treat leaving as a lock signal on real native platforms.
95
115
  const subscription = RNAppState.addEventListener("change", (nextState) => {
96
- if (nextState === "background" && Platform.OS !== "web") {
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
  }