Skip to content

Commit 5f1a04a

Browse files
committed
Grey out unavailable access-control options and auto-fallback in example; update README and deps
- Example: add isOptionAvailable() to project live device capability checks - Prevent selecting unavailable access controls, show "Unavailable on this device" note - Auto-select strongest available fallback when current selection becomes unsupported - UI: add info banner and disabled styles; improve accessibility state and press handling - README: clarify "Class 3 / StrongBox" wording, describe access-control selector behaviour, remove runtime requirements blurb - Bump example/root dev deps and react-native/react-native-nitro-modules versions and refresh yarn.lock
1 parent 08a56d6 commit 5f1a04a

5 files changed

Lines changed: 314 additions & 82 deletions

File tree

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Modern secure storage for React Native, powered by Nitro Modules. Version 6 ship
3434
## 🚀 Highlights
3535

3636
- Headless Nitro hybrid object with a simple Promise-based API (`setItem`, `getItem`, `hasItem`, `getAllItems`, `clearService`).
37-
- Automatic security negotiation: prefers Secure Enclave (iOS) or StrongBox/biometric-protected keys (Android) with graceful fallbacks.
37+
- Automatic security negotiation: locks onto Secure Enclave (iOS) or Class 3 / StrongBox biometrics (Android) with graceful fallbacks when hardware is limited.
3838
- Unified metadata reporting (security level, backend, access control, timestamp) for every stored secret.
3939
- Friendly example app showcasing prompts, metadata inspection, and per-platform capability detection.
4040
- First-class TypeScript definitions and tree-shakeable distribution via `react-native-builder-bob`.
@@ -50,14 +50,6 @@ Modern secure storage for React Native, powered by Nitro Modules. Version 6 ship
5050
| Android | API 23 (Marshmallow) | StrongBox detection requires API 28+; biometrics fall back to device credential when unavailable. |
5151
| Windows || Removed in v6. Earlier versions may still work but are no longer maintained. |
5252

53-
**Runtime requirements**
54-
- React Native `0.82` or newer (Nitro Modules baseline).
55-
- Node.js `18` or newer.
56-
- `react-native-nitro-modules` `>=0.30.0`.
57-
58-
> [!TIP]
59-
> Pair this module with [`react-native-quick-crypto`](https://github.com/mCodex/react-native-quick-crypto) when you need high-performance hashing alongside secure storage.
60-
6153
## ⚙️ Installation
6254

6355
```bash
@@ -193,6 +185,9 @@ Always validate security behavior on the physical devices you ship to customers.
193185

194186
Explore the full feature set with the bundled example app. It showcases capability detection, metadata inspection, and error surface normalization for every API call.
195187

188+
- The access-control selector now projects live device capabilities, greying out policies that require unavailable hardware and auto-picking the strongest viable guard.
189+
- Android Class 3 biometrics are applied automatically—no more manual toggle—while older devices fall back to the most secure authenticator they expose.
190+
196191
> [!TIP]
197192
> Prefer Expo? The same Nitro module works inside bare Expo projects—just install via `expo install` and run the commands below from `example/`.
198193

example/App.tsx

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,28 @@ function App(): React.JSX.Element {
208208
);
209209
const [pending, setPending] = useState(false);
210210

211+
const isOptionAvailable = useCallback(
212+
(value: AccessControl) => {
213+
if (!availability) {
214+
return true;
215+
}
216+
217+
switch (value) {
218+
case 'secureEnclaveBiometry':
219+
return availability.secureEnclave || availability.strongBox;
220+
case 'biometryCurrentSet':
221+
case 'biometryAny':
222+
return availability.biometry;
223+
case 'devicePasscode':
224+
return availability.deviceCredential;
225+
case 'none':
226+
default:
227+
return true;
228+
}
229+
},
230+
[availability],
231+
);
232+
211233
const normalizedService = useMemo(() => {
212234
const trimmed = service.trim();
213235
return trimmed.length > 0 ? trimmed : DEFAULT_SERVICE;
@@ -280,6 +302,24 @@ function App(): React.JSX.Element {
280302
void refreshItems();
281303
}, [refreshAvailability, refreshItems]);
282304

305+
useEffect(() => {
306+
if (!availability) {
307+
return;
308+
}
309+
310+
if (isOptionAvailable(selectedAccessControl)) {
311+
return;
312+
}
313+
314+
const fallback = ACCESS_CONTROL_OPTIONS.find(option =>
315+
isOptionAvailable(option.value),
316+
);
317+
318+
if (fallback) {
319+
setSelectedAccessControl(fallback.value);
320+
}
321+
}, [availability, isOptionAvailable, selectedAccessControl]);
322+
283323
const execute = useCallback(
284324
async (task: () => Promise<void>) => {
285325
if (pending) {
@@ -504,32 +544,56 @@ function App(): React.JSX.Element {
504544
title="Security & authentication"
505545
subtitle="Tune access control, prompts, and cross-platform behaviour"
506546
>
547+
<Text style={styles.infoNote}>
548+
The native layer automatically upgrades to the strongest guard this
549+
device supports. Options shown in grey are unavailable on the
550+
current hardware.
551+
</Text>
507552
<View style={styles.accessOptionsContainer}>
508553
{ACCESS_CONTROL_OPTIONS.map(option => {
509554
const selected = option.value === selectedAccessControl;
555+
const available = isOptionAvailable(option.value);
556+
const disabled = !available;
510557
return (
511558
<Pressable
512559
key={option.value}
513560
accessibilityRole="radio"
514-
accessibilityState={{ selected }}
515-
onPress={() => setSelectedAccessControl(option.value)}
561+
accessibilityState={{ selected, disabled }}
562+
onPress={() => {
563+
if (!available) {
564+
return;
565+
}
566+
setSelectedAccessControl(option.value);
567+
}}
516568
style={({ pressed }) => [
517569
styles.accessOption,
518570
selected && styles.accessOptionSelected,
519-
pressed && styles.accessOptionPressed,
571+
pressed && !disabled && styles.accessOptionPressed,
572+
disabled && styles.accessOptionDisabled,
520573
]}
521574
>
522575
<Text
523576
style={[
524577
styles.accessOptionLabel,
525578
selected && styles.accessOptionLabelSelected,
579+
disabled && styles.accessOptionLabelDisabled,
526580
]}
527581
>
528582
{option.label}
529583
</Text>
530-
<Text style={styles.accessOptionDescription}>
584+
<Text
585+
style={[
586+
styles.accessOptionDescription,
587+
disabled && styles.accessOptionDescriptionDisabled,
588+
]}
589+
>
531590
{option.description}
532591
</Text>
592+
{disabled ? (
593+
<Text style={styles.accessOptionUnavailable}>
594+
Unavailable on this device
595+
</Text>
596+
) : null}
533597
</Pressable>
534598
);
535599
})}
@@ -774,6 +838,18 @@ const styles = StyleSheet.create({
774838
color: '#4b5563',
775839
fontSize: 15,
776840
},
841+
infoNote: {
842+
color: '#1e3a8a',
843+
fontSize: 13,
844+
lineHeight: 18,
845+
backgroundColor: '#e0f2fe',
846+
borderRadius: 14,
847+
borderWidth: 1,
848+
borderColor: '#bfdbfe',
849+
paddingHorizontal: 16,
850+
paddingVertical: 12,
851+
marginBottom: 16,
852+
},
777853
field: {
778854
marginBottom: 20,
779855
},
@@ -823,6 +899,10 @@ const styles = StyleSheet.create({
823899
accessOptionPressed: {
824900
opacity: 0.9,
825901
},
902+
accessOptionDisabled: {
903+
borderColor: '#e5e7eb',
904+
backgroundColor: '#f1f5f9',
905+
},
826906
accessOptionLabel: {
827907
color: '#1f2937',
828908
fontSize: 15,
@@ -831,12 +911,25 @@ const styles = StyleSheet.create({
831911
accessOptionLabelSelected: {
832912
color: '#1d4ed8',
833913
},
914+
accessOptionLabelDisabled: {
915+
color: '#9ca3af',
916+
},
834917
accessOptionDescription: {
835918
color: '#6b7280',
836919
fontSize: 13,
837920
lineHeight: 19,
838921
marginTop: 6,
839922
},
923+
accessOptionDescriptionDisabled: {
924+
color: '#9ca3af',
925+
},
926+
accessOptionUnavailable: {
927+
color: '#ef4444',
928+
fontSize: 12,
929+
fontWeight: '600',
930+
marginTop: 8,
931+
letterSpacing: 0.2,
932+
},
840933
toggleCard: {
841934
flexDirection: 'row',
842935
alignItems: 'center',

example/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
"dependencies": {
1414
"@react-native/new-app-screen": "0.82.1",
1515
"react": "19.1.1",
16-
"react-native": "0.82.0",
17-
"react-native-nitro-modules": "0.31.1",
16+
"react-native": "0.82.1",
17+
"react-native-nitro-modules": "0.31.2",
1818
"react-native-safe-area-context": "^5.6.1"
1919
},
2020
"devDependencies": {
21-
"@babel/core": "^7.28.4",
22-
"@babel/preset-env": "^7.28.3",
21+
"@babel/core": "^7.28.5",
22+
"@babel/preset-env": "^7.28.5",
2323
"@babel/runtime": "^7.28.4",
2424
"@react-native-community/cli": "20.0.2",
2525
"@react-native-community/cli-platform-android": "20.0.2",

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@
5353
},
5454
"devDependencies": {
5555
"@eslint/compat": "^1.4.0",
56-
"@eslint/js": "^9.37.0",
56+
"@eslint/js": "^9.38.0",
5757
"@jamesacarr/eslint-formatter-github-actions": "^0.2.0",
5858
"@semantic-release/changelog": "^6.0.3",
5959
"@semantic-release/git": "^10.0.1",
6060
"@types/jest": "^30.0.0",
61-
"@types/react": "19.1.x",
61+
"@types/react": "19.2.x",
6262
"conventional-changelog-conventionalcommits": "^9.1.0",
63-
"eslint": "^9.37.0",
63+
"eslint": "^9.38.0",
6464
"eslint-config-airbnb": "^19.0.4",
6565
"eslint-config-prettier": "^10.1.8",
6666
"eslint-import-resolver-typescript": "^4.4.4",
@@ -69,18 +69,18 @@
6969
"eslint-plugin-jsx-a11y": "^6.10.2",
7070
"eslint-plugin-prettier": "^5.5.4",
7171
"eslint-plugin-react": "^7.37.5",
72-
"eslint-plugin-react-hooks": "^7.0.0",
72+
"eslint-plugin-react-hooks": "^7.0.1",
7373
"globals": "^16.4.0",
7474
"jiti": "^2.6.1",
75-
"nitrogen": "0.31.1",
75+
"nitrogen": "0.31.2",
7676
"prettier": "^3.6.2",
7777
"react": "19.1.1",
7878
"react-native": "0.82",
7979
"react-native-builder-bob": "^0.40.13",
80-
"react-native-nitro-modules": "0.31.1",
81-
"semantic-release": "^25.0.0",
80+
"react-native-nitro-modules": "0.31.2",
81+
"semantic-release": "^25.0.1",
8282
"typescript": "^5.9.3",
83-
"typescript-eslint": "^8.46.1"
83+
"typescript-eslint": "^8.46.2"
8484
},
8585
"peerDependencies": {
8686
"react": "*",

0 commit comments

Comments
 (0)