Skip to content

Commit

Permalink
feat: create extractor component
Browse files Browse the repository at this point in the history
  • Loading branch information
1fabiopereira committed Jun 19, 2023
1 parent b98c4d8 commit 04870ad
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 4 deletions.
239 changes: 239 additions & 0 deletions src/extractors/core/Extractor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import {
Platform,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import Modal from 'react-native-modal';

import { BaseExtractor } from './Base';
import { Chain, ChainLink } from '../../chains';
import Styles from './Styles';

import type {
Action,
Patterns,
TransientObject,
WithPassword,
} from '../../types';

type ExtractorProps = {
cancel?: string;
fromIntent?: boolean;
onResult: (data: TransientObject) => void;
submit?: string;
password?: string;
patterns?: Patterns;
placeholder?: string;
title?: string;
uri?: string;
};

export const Extractor: React.FC<ExtractorProps> = memo(
({
cancel = 'Cancel',
fromIntent,
onResult,
submit = 'Open',
patterns,
placeholder = 'Password',
title = 'This file is protected',
uri,
}) => {
const [locker, setLocker] = useState<string | undefined>();
const [value, setValue] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [visibility, setVisibility] = useState(false);

/**
* Close modal
*/
const close = () => {
setVisibility(false);
};

/**
* Triggers callbacks when password changes
*/
const changePassword = useCallback(() => {
close();
setPassword(value);
}, [value]);

/**
* Verifies if file exists based on URI gave
* or got from intent provider (Android only)
*/
const file = useCallback(
async (data: TransientObject): Promise<TransientObject> => {
if (data.uri) {
const path = await BaseExtractor.setUri(data.uri);

if (path) {
return { ...data, uri: path };
}

throw new Error(
`Invalid uri: '${data.uri}'. We cannot find the file.`
);
}

// From Intent (android only)
if (Platform.OS === 'android' && fromIntent) {
const path = await BaseExtractor.getUri();

if (path) {
return { ...data, uri: path };
}

// eslint-disable-next-line prettier/prettier
throw new Error('Cannot get URI from Intent. Check your app Intent provider config.');
}

throw new Error('Could not perfom extraction without URI.');
},
[fromIntent]
);

/**
* Checks if params satisfies full specification
* to proceed with data extraction
*/
const check = async (data: TransientObject): Promise<TransientObject> => {
const canIExtract = await BaseExtractor.canIExtract();

if (canIExtract) {
return data;
}

throw new Error('You cannot continue with extraction.');
};

/**
* Checks if file is encrypted
*/
const encrypted = async (
data: TransientObject
): Promise<TransientObject> => {
const isEncrypted = await BaseExtractor.isEncrypted();
return { ...data, isEncrypted };
};

/**
* Counts the number of pages
*/
const pages = useCallback(
async (data: WithPassword<TransientObject>): Promise<TransientObject> => {
const total = await BaseExtractor.getNumberOfPages(data.password);
return { ...data, pages: total };
},
[]
);

/**
* Applies matches
*/
const matches = useCallback(
async (data: WithPassword<TransientObject>): Promise<TransientObject> => {
const text = !data.patterns
? await BaseExtractor.getText(data.password)
: await BaseExtractor.getTextWithPattern(
data.patterns,
data.password
);

return { ...data, text };
},
[]
);

/**
* Verifies if needs user interaction to provide password
* and shows component if needed
*/
const verify = useCallback(async () => {
try {
const data: TransientObject = await new Chain([
new ChainLink(file as Action),
new ChainLink(check as Action),
new ChainLink(encrypted as Action),
]).exec({ uri, patterns });

if (data.isEncrypted && !password) {
setVisibility(true);
}

return data;
} catch (error) {
console.warn(error);
return null;
}
}, [file, uri, patterns, password]);

/**
* Execute data extraction
*/
const exec = useCallback(async (): Promise<void> => {
const start = new Date().getTime();
const data = await verify();

if (
(data?.isEncrypted && password && !visibility) ||
(!data?.isEncrypted && !visibility)
) {
const result = await new Chain([
new ChainLink(pages as Action),
new ChainLink(matches as Action),
]).exec({ ...data, password });

const finish = new Date().getTime();

onResult({ ...result, duration: `${finish - start}ms` });
}
}, [matches, onResult, pages, password, verify, visibility]);

/**
* Verifies if can re-render component or runs data extraction
*/
useEffect(() => {
const lock = `${uri}|${patterns}|${fromIntent}|${password}`;
const withPatterns = (uri && patterns) || (fromIntent && patterns);
const withoutPatterns = uri || fromIntent;

if (lock !== locker && (withPatterns || withoutPatterns)) {
setLocker(lock);
exec();
}
}, [exec, uri, patterns, fromIntent, locker, password]);

return (
<Modal
hideModalContentWhileAnimating={false}
isVisible={visibility}
onBackButtonPress={close}
onBackdropPress={close}
useNativeDriver
>
<View style={Styles.Container}>
<Text style={Styles.Title}>{title}</Text>
<TextInput
onChangeText={setValue}
placeholder={placeholder}
secureTextEntry
underlineColorAndroid="gray"
/>
<View style={Styles.Row}>
<TouchableOpacity style={Styles.Button} onPress={close}>
<Text style={Styles.Text}>{cancel}</Text>
</TouchableOpacity>
<TouchableOpacity style={Styles.Button} onPress={changePassword}>
<Text style={Styles.Text}>{submit}</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
);
30 changes: 30 additions & 0 deletions src/extractors/core/Styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';

export default StyleSheet.create({
Container: {
backgroundColor: 'white',
borderRadius: 4,
height: 155,
padding: 15,
},

Title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},

Row: {
flexDirection: 'row',
justifyContent: 'flex-end',
},

Button: {
padding: 16,
textAlign: 'center',
},

Text: {
color: 'gray',
},
});
8 changes: 4 additions & 4 deletions src/patterns/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const Common = {
Email: ['(\\S+@\\w+\\.\\w+)'],
Email: [/(\S+@\w+\.\w+)/],
};

const Brazil = {
BankSlip: [
'([0-9]{5})\\.([0-9]{5})\\s([0-9]{5})\\.([0-9]{6})\\s([0-9]{5})\\.([0-9]{6})\\s([0-9])\\s([0-9]{14})', // Banking - Typeable line
'([0-9]{12})\\s([0-9]{12})\\s([0-9]{12})\\s([0-9]{12})', // Tax revenues - Bar code
'([0-9]{11})-([0-9])\\s([0-9]{11})-([0-9])\\s([0-9]{11})-([0-9])\\s([0-9]{11})-([0-9])', // Tax revenues - Typeable line
/([0-9]{5})\.([0-9]{5})\s([0-9]{5})\.([0-9]{6})\s([0-9]{5})\.([0-9]{6})\s([0-9])\s([0-9]{14})/, // Banking - Typeable line
/([0-9]{12})\s([0-9]{12})\s([0-9]{12})\s([0-9]{12})/, // Tax revenues - Bar code
/([0-9]{11})-([0-9])\s([0-9]{11})-([0-9])\s([0-9]{11})-([0-9])\s([0-9]{11})-([0-9])/, // Tax revenues - Typeable line
],
};

Expand Down

0 comments on commit 04870ad

Please sign in to comment.