Skip to content

Commit 1dc4825

Browse files
authored
feat: Adding a hasItem method (#259)
* fix: added a has-item method * fix: example cleanup
1 parent 4f9af66 commit 1dc4825

5 files changed

Lines changed: 83 additions & 4 deletions

File tree

android/src/main/java/dev/mcodex/RNSensitiveInfoModule.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ public void getItem(String key, ReadableMap options, Promise pm) {
195195
}
196196
}
197197

198+
@ReactMethod
199+
public void hasItem(String key, ReadableMap options, Promise pm) {
200+
String name = sharedPreferences(options);
201+
202+
String value = prefs(name).getString(key, null);
203+
204+
pm.resolve(value != null ? true : false);
205+
}
206+
198207
@ReactMethod
199208
public void setItem(String key, String value, ReadableMap options, Promise pm) {
200209

example/src/pages/Home/index.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useState } from 'react';
2-
import { View, Button, Alert, Text } from 'react-native';
2+
import { View, Button, Alert, Text, SafeAreaView } from 'react-native';
33
import SInfo from 'react-native-sensitive-info';
44

55
const Home: React.FC = () => {
@@ -45,6 +45,27 @@ const Home: React.FC = () => {
4545
}
4646
}, []);
4747

48+
const hasTouchIDItem = useCallback(async () => {
49+
50+
try {
51+
const hasItem = await SInfo.hasItem(
52+
'touchIdItem',
53+
{
54+
sharedPreferencesName: 'exampleApp',
55+
keychainService: 'exampleApp',
56+
kSecAccessControl: 'kSecAccessControlBiometryAny', // Enabling FaceID
57+
touchID: true,
58+
showModal: true,
59+
},
60+
);
61+
62+
Alert.alert(hasItem ? "Item is present" : "item is not present");
63+
} catch (ex) {
64+
Alert.alert('Error', ex.message);
65+
}
66+
}, []);
67+
68+
4869
const getTouchIDItem = useCallback(async () => {
4970
const deviceHasSensor = await SInfo.isSensorAvailable();
5071

@@ -94,7 +115,7 @@ const Home: React.FC = () => {
94115
runTest();
95116

96117
return (
97-
<View>
118+
<SafeAreaView style={{ margin: 10 }}>
98119
<Button
99120
title="Add item using setItem"
100121
onPress={handleAddUsingSetItemOnPress}
@@ -109,9 +130,10 @@ const Home: React.FC = () => {
109130
/>
110131

111132
<Button title="Get TouchID Data" onPress={getTouchIDItem} />
133+
<Button title="Has TouchID Data" onPress={hasTouchIDItem} />
112134

113135
<Text>{logText}</Text>
114-
</View>
136+
</SafeAreaView>
115137
);
116138
};
117139
export default Home;

index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export declare function getItem(
5151
key: string,
5252
options: RNSensitiveInfoOptions,
5353
): Promise<string>;
54+
export declare function hasItem(
55+
key: string,
56+
options: RNSensitiveInfoOptions,
57+
): Promise<boolean>;
5458

5559
interface SensitiveInfoEntry {
5660
key: string;

ios/RNSensitiveInfo/RNSensitiveInfo.m

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,51 @@ - (NSString *)messageForError:(NSError *)error
226226
});
227227
}
228228

229+
RCT_EXPORT_METHOD(hasItem:(NSString *)key options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
230+
NSString * keychainService = [RCTConvert NSString:options[@"keychainService"]];
231+
if (keychainService == NULL) {
232+
keychainService = @"app";
233+
}
234+
235+
// Create dictionary of search parameters
236+
NSMutableDictionary* query = [NSMutableDictionary dictionaryWithObjectsAndKeys:(__bridge id)(kSecClassGenericPassword), kSecClass,
237+
keychainService, kSecAttrService,
238+
key, kSecAttrAccount,
239+
kSecAttrSynchronizableAny, kSecAttrSynchronizable,
240+
kCFBooleanTrue, kSecReturnAttributes,
241+
kCFBooleanTrue, kSecReturnData,
242+
nil];
243+
244+
245+
dispatch_async(dispatch_get_main_queue(), ^{
246+
if (UIApplication.sharedApplication.protectedDataAvailable) {
247+
// Look up server in the keychain
248+
NSDictionary* found = nil;
249+
CFTypeRef foundTypeRef = NULL;
250+
OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef*)&foundTypeRef);
251+
252+
if (osStatus != noErr && osStatus != errSecItemNotFound) {
253+
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
254+
reject([NSString stringWithFormat:@"%ld",(long)error.code], [self messageForError:error], nil);
255+
return;
256+
}
257+
258+
found = (__bridge NSDictionary*)(foundTypeRef);
259+
if (!found) {
260+
resolve(@(FALSE));
261+
} else {
262+
// Found
263+
resolve(@(TRUE));
264+
}
265+
} else {
266+
// TODO: could change to instead of erroring out, listen for protectedDataDidBecomeAvailable and call getItemWIthQuery when it does
267+
// Experiment for now by returning an error and let the js side retry
268+
reject(@"protected_data_unavailable", @"Protected data not available yet. Retry operation", nil);
269+
}
270+
});
271+
}
272+
273+
229274
- (void)getItemWithQuery:(NSDictionary *)query resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
230275
// Look up server in the keychain
231276
NSDictionary* found = nil;
@@ -245,7 +290,6 @@ - (void)getItemWithQuery:(NSDictionary *)query resolver:(RCTPromiseResolveBlock)
245290
// Found
246291
NSString* value = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding];
247292
resolve(value);
248-
249293
}
250294
}
251295

0 commit comments

Comments
 (0)