From 04870ad69474b75aaefd3a9d186967427dcc51de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Pereira?= Date: Mon, 19 Jun 2023 17:20:47 -0300 Subject: [PATCH] feat: create extractor component --- src/extractors/core/Extractor.tsx | 239 ++++++++++++++++++++++++++++++ src/extractors/core/Styles.ts | 30 ++++ src/patterns/index.ts | 8 +- 3 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 src/extractors/core/Extractor.tsx create mode 100644 src/extractors/core/Styles.ts diff --git a/src/extractors/core/Extractor.tsx b/src/extractors/core/Extractor.tsx new file mode 100644 index 0000000..f6357e3 --- /dev/null +++ b/src/extractors/core/Extractor.tsx @@ -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 = memo( + ({ + cancel = 'Cancel', + fromIntent, + onResult, + submit = 'Open', + patterns, + placeholder = 'Password', + title = 'This file is protected', + uri, + }) => { + const [locker, setLocker] = useState(); + const [value, setValue] = useState(); + const [password, setPassword] = useState(); + 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 => { + 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 => { + 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 => { + const isEncrypted = await BaseExtractor.isEncrypted(); + return { ...data, isEncrypted }; + }; + + /** + * Counts the number of pages + */ + const pages = useCallback( + async (data: WithPassword): Promise => { + const total = await BaseExtractor.getNumberOfPages(data.password); + return { ...data, pages: total }; + }, + [] + ); + + /** + * Applies matches + */ + const matches = useCallback( + async (data: WithPassword): Promise => { + 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 => { + 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 ( + + + {title} + + + + {cancel} + + + {submit} + + + + + ); + } +); diff --git a/src/extractors/core/Styles.ts b/src/extractors/core/Styles.ts new file mode 100644 index 0000000..51c537b --- /dev/null +++ b/src/extractors/core/Styles.ts @@ -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', + }, +}); diff --git a/src/patterns/index.ts b/src/patterns/index.ts index a2f8c54..4418df1 100644 --- a/src/patterns/index.ts +++ b/src/patterns/index.ts @@ -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 ], };