diff --git a/package.json b/package.json index 02913f3..9333c6b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "prepare": "bob build", "example": "yarn --cwd example", "bootstrap": "yarn example && yarn && yarn example pods", - "build": "bob build" + "build": "bob build", + "docs-gen": "node scripts/doc-gen/components-json-gen.js && node scripts/doc-gen/markdown-gen.js" }, "keywords": [ "react-native", @@ -129,6 +130,7 @@ "dependencies": { "jwt-decode": "^3.1.2", "lodash": "^4.17.21", + "react-docgen-typescript": "^2.2.2", "react-native-uuid": "^2.0.1", "set-value": "^4.1.0" } diff --git a/scripts/doc-gen/components-json-gen.js b/scripts/doc-gen/components-json-gen.js new file mode 100644 index 0000000..c8fcf64 --- /dev/null +++ b/scripts/doc-gen/components-json-gen.js @@ -0,0 +1,68 @@ +/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs'); +const path = require('path'); +const docgen = require('react-docgen-typescript'); + +const tsConfigParser = docgen.withCustomConfig('./tsconfig.json', { + savePropValueAsString: true, + skipChildrenPropWithoutDoc: false, + propFilter: { + skipPropsWithoutDoc: false, + skipPropsWithName: 'children', + }, + shouldExtractLiteralValuesFromEnum: true, +}); + +const paths = ['components', 'hooks']; +const excludeDirs = []; + +function getFiles(dir, files = []) { + const fileNames = fs.readdirSync(dir); + + fileNames.forEach((fileName) => { + const filePath = path.join(dir, fileName); + const stat = fs.statSync(filePath); + if (stat.isDirectory() && !excludeDirs.includes(fileName)) { + getFiles(filePath, files); + } else if (fileName === 'index.tsx' || fileName === 'index.ts') { + files.push(filePath); + } + }); + + return files; +} + +function generateDocumentationJson() { + const docJson = {}; + const rootPath = path.join(__dirname, '../../'); + + paths.forEach((item) => { + const pathJsonArray = []; + const folderPath = path.join(rootPath, 'src', item); + const files = getFiles(folderPath); + files.forEach((file) => { + const relativePath = path.relative(rootPath, file); + const docs = tsConfigParser.parse(relativePath); + pathJsonArray.push(docs[0]); + }); + docJson[item] = pathJsonArray; + }); + return docJson; +} + +function createDocJsonFile(json) { + const dirPath = path.join(__dirname, '../../', 'docs', 'json'); + const filePath = path.join(dirPath, 'components.json'); + + // Create docs/json folder if it doesn't exist + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + + // Write to components.json file + fs.writeFileSync(filePath, JSON.stringify(json, null, 2)); + console.log('Json documentation is generated at docs/json'); +} + +createDocJsonFile(generateDocumentationJson()); diff --git a/scripts/doc-gen/markdown-gen.js b/scripts/doc-gen/markdown-gen.js new file mode 100644 index 0000000..db11abd --- /dev/null +++ b/scripts/doc-gen/markdown-gen.js @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const components = require('../../docs/json/components.json'); +const fs = require('fs'); +const path = require('path'); + +function formatTypeColumn(str, propType) { + if (!str) { + return ''; + } + const parts = str.split('|').map((s) => s.trim()); + if (parts.length === 1) { + return `\`${str}\``; + } + if (!propType) { + return `\`${parts[0]}\``; + } + return `\`${parts[0]} / ${parts[1]}\``; +} + +// Output directory for Markdown files +const outputDir = './docs/markdown'; + +// Overview page to list elements +let overviewContent = ` +{% env enable="reactNativeSdkRef" %} + +# React-Native + +Some documentation for the overview page. +`; + +// Loop through each component in the JSON object +Object.keys(components).forEach((key) => { + overviewContent += `\n## ${ + key == 'core' + ? 'Skyflow Provider' + : key == 'elements' + ? 'Components' + : key.charAt(0).toUpperCase() + key.slice(1) + }\n\n`; + + components[key] + .filter((component) => component) + .forEach((component) => { + // Create the Markdown file path based on the component name + const componentPath = path.join( + outputDir, + key, + `${component.displayName}.md` + ); + + const name = `${component.displayName}`; + overviewContent += `- [${name}](/sdks/skyflow-react-native/${key}/${name})\n`; + + const sortedProps = Object.entries(component.props) + .sort(([_, propA], [__, propB]) => { + if (propA.required && !propB.required) { + return -1; // propA comes before propB + } else if (!propA.required && propB.required) { + return 1; // propB comes before propA + } + return 0; // no change in order + }) + .reduce((sorted, [key, value]) => { + sorted[key] = value; + return sorted; + }, {}); + + // Generate the Markdown content for the component + let markdownContent = `--- +id: ${component.displayName} +title: ${component.displayName} +sidebar_label: ${component.displayName} +--- + +{% env enable="reactNativeSdkRef" %} + +# ${component.displayName} + +${component.description} + +## Import + +\`\`\` +import {${component.displayName}} from 'skyflow-react-native'; +\`\`\` +`; + const propsDetails = ` +## Props + +| Name | Type | Description | Required | +|-------------------------|----------------------|---------------------------------------------------------|------------------| +${Object.keys(sortedProps) + .map((propName) => { + const prop = sortedProps[propName]; + return `| ${prop.name} | ${formatTypeColumn( + prop.type.name, + prop.required + )} | ${prop.description} | ${prop.required} |`; + }) + .join('\n')} + +`; + if (Object.keys(component.props).length) { + markdownContent += propsDetails; + } + + if (Object.keys(component.tags).length > 0 && component.tags['returns']) { + markdownContent += `\n## Returns\n${component.tags['returns']}\n\n`; + } + + markdownContent += '{% /env %}'; + const folderPath = path.dirname(componentPath); + + // Create the folder if it doesn't exist + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + // Write the Markdown content to the file + fs.writeFileSync(componentPath, markdownContent); + }); +}); +overviewContent += '\n{% /env %}'; +fs.writeFileSync(path.join(outputDir, 'Overview.md'), overviewContent); +console.log('markdown files generated at docs/markdown'); diff --git a/src/components/CardHolderNameElement/index.tsx b/src/components/CardHolderNameElement/index.tsx index e9ec317..97ded3a 100644 --- a/src/components/CardHolderNameElement/index.tsx +++ b/src/components/CardHolderNameElement/index.tsx @@ -9,6 +9,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect cardholder names. + */ const CardHolderNameElement: React.FC = ({ container, options = { required: false }, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/CardNumberElement/index.tsx b/src/components/CardNumberElement/index.tsx index 28faa37..1aa9c81 100644 --- a/src/components/CardNumberElement/index.tsx +++ b/src/components/CardNumberElement/index.tsx @@ -10,6 +10,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect card numbers. + */ const CardNumberElement: React.FC = ({ container, options = { required: false }, ...rest }) => { const [element, setElement] = React.useState(undefined); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/CvvElement/index.tsx b/src/components/CvvElement/index.tsx index dddbe8d..9a29d21 100644 --- a/src/components/CvvElement/index.tsx +++ b/src/components/CvvElement/index.tsx @@ -9,6 +9,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect CVVs. + */ const CvvElement: React.FC = ({ container, options = { requried: false }, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/ExpirationDateElement/index.tsx b/src/components/ExpirationDateElement/index.tsx index 04cd240..c828d3d 100644 --- a/src/components/ExpirationDateElement/index.tsx +++ b/src/components/ExpirationDateElement/index.tsx @@ -11,6 +11,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect expiration dates. + */ const ExpirationDateElement: React.FC = ({ container, options, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/ExpirationMonthElement/index.tsx b/src/components/ExpirationMonthElement/index.tsx index 8092770..01f9be8 100644 --- a/src/components/ExpirationMonthElement/index.tsx +++ b/src/components/ExpirationMonthElement/index.tsx @@ -9,6 +9,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect expiration month values. + */ const ExpirationMonthElement: React.FC = ({ container, options = { required: false }, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/ExpirationYearElement/index.tsx b/src/components/ExpirationYearElement/index.tsx index c21439e..badc79f 100644 --- a/src/components/ExpirationYearElement/index.tsx +++ b/src/components/ExpirationYearElement/index.tsx @@ -12,6 +12,9 @@ import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect expiration year values. + */ const ExpirationYearElement: React.FC = ({ container, options, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/InputFieldElement/index.tsx b/src/components/InputFieldElement/index.tsx index 0825173..58837d0 100644 --- a/src/components/InputFieldElement/index.tsx +++ b/src/components/InputFieldElement/index.tsx @@ -9,6 +9,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect arbitrary values. + */ const InputFieldElement: React.FC = ({ container, options = { required: false }, ...rest }) => { const [element, setElement] = React.useState(undefined); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/PinElement/index.tsx b/src/components/PinElement/index.tsx index 6bab3dc..1292721 100644 --- a/src/components/PinElement/index.tsx +++ b/src/components/PinElement/index.tsx @@ -9,6 +9,9 @@ import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; +/** + * Element to collect PIN values. + */ const PinElement: React.FC = ({ container, options = { required: false }, ...rest }) => { const [element, setElement] = React.useState(); const [elementValue, setElementValue] = React.useState(''); diff --git a/src/components/RevealElement/index.tsx b/src/components/RevealElement/index.tsx index d08872a..6487390 100644 --- a/src/components/RevealElement/index.tsx +++ b/src/components/RevealElement/index.tsx @@ -8,7 +8,9 @@ import { RevealElementProps } from "../../utils/constants" import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; - +/** + * Configuration for Reveal Elements. + */ const RevealElement: React.FC = ({ container, label, ...rest }) => { const [element, setElement] = React.useState(undefined); const [errorText, setErrorText] = React.useState(''); diff --git a/src/components/SkyflowProvider/index.tsx b/src/components/SkyflowProvider/index.tsx index fb50ab3..9515871 100644 --- a/src/components/SkyflowProvider/index.tsx +++ b/src/components/SkyflowProvider/index.tsx @@ -6,11 +6,15 @@ import Skyflow from '../../core/Skyflow'; import { IConfig, SkyflowConfigIntialState } from '../../utils/constants'; export interface ISkyflowProvider { + /** Configuration object for SkyflowProvider. */ config: IConfig, } export const skyflowContext = React.createContext(null); +/** + * Sets up the Skyflow context using the provided configuration. + */ const SkyflowProvider: React.FC> = ({children,config}): JSX.Element => { const skyflow = new Skyflow(config); return {children} diff --git a/src/hooks/useCollectContainer/index.ts b/src/hooks/useCollectContainer/index.ts index 62e994b..26b46b6 100644 --- a/src/hooks/useCollectContainer/index.ts +++ b/src/hooks/useCollectContainer/index.ts @@ -4,6 +4,10 @@ import useSkyflowContext from '../../components/SkyflowProvider/hook'; import CollectContainer from '../../core/CollectContainer'; +/** + * Container for Collect Elements. + * @returns Returns the CollectContainer instance. + */ const useCollectContainer = () => { const skyflowClient = useSkyflowContext(); return new CollectContainer(skyflowClient); diff --git a/src/hooks/useRevealContainer/index.ts b/src/hooks/useRevealContainer/index.ts index e5c8c50..4d60be8 100644 --- a/src/hooks/useRevealContainer/index.ts +++ b/src/hooks/useRevealContainer/index.ts @@ -4,6 +4,10 @@ import useSkyflowContext from '../../components/SkyflowProvider/hook'; import RevealContainer from '../../core/RevealContainer'; +/** + * Container for Reveal Elements. + * @returns Returns the RevealContainer instance. + */ const useRevealContainer = () => { const skyflowClient = useSkyflowContext(); return new RevealContainer(skyflowClient); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 6aa99fd..b6cd560 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -4,10 +4,17 @@ import type CollectContainer from '../../core/CollectContainer'; import RevealContainer from '../../core/RevealContainer'; +/** + * Configuration for connecting to the Skyflow vault. + */ export interface IConfig { + /** ID of the vault to connect to. */ vaultID: string; + /** URL of the vault to connect to. */ vaultURL: string; + /** Function that retrieves a Skyflow bearer token from your backend. */ getBearerToken: () => Promise; + /** Additional configuration options. */ options?: Record; } @@ -37,23 +44,40 @@ export interface CollectElementInput { } export interface CollectElementProps { + /** Table that the data belongs to. */ table: string; + /** Column that the data belongs to. */ column: string; + /** Type of the container. */ container: CollectContainer; + /** Label for the element. */ label?: string; + /** Placeholder text for the element. */ placeholder?: string; + /** Input validation rules for the element. */ validations?: IValidationRule[]; + /** Function to call when the onChange event triggers. */ onChange?: Function; + /** Function to call when the onReady event triggers. */ onReady?: Function; + /** Function to call when the onBlur event triggers. */ onBlur?: Function; + /** Function to call when the onFocus event triggers. */ onFocus?: Function; + /** Additional configuration options. */ options?: Record; + /** Styles for the element.*/ inputStyles?: CollectInputStylesVariant; + /** Styles for the element's label. */ labelStyles?: CollectLabelStylesVariant; + /** Styles for the element's error text. */ errorTextStyles?: StylesBaseVariant; containerMethods?: Record; } +/** + * Supported element types. + */ export enum ElementType { CVV = 'CVV', EXPIRATION_DATE = 'EXPIRATION_DATE', @@ -81,6 +105,9 @@ export enum ContentType { FORMDATA = 'multipart/form-data', } +/** + * Supported log levels. + */ export enum LogLevel { WARN = 'WARN', INFO = 'INFO', @@ -88,6 +115,9 @@ export enum LogLevel { ERROR = 'ERROR', } +/** + * Supported environments. + */ export enum Env { DEV = 'DEV', PROD = 'PROD', @@ -102,13 +132,21 @@ export interface RevealElementInput { } export interface RevealElementProps { + /** The actual data token. */ token: string; + /** The reveal container. */ container: RevealContainer; + /** Label for the form element. */ label?: string; + /** Alternative text for the Reveal Element. */ altText?: string; + /** Styles for the element. */ inputStyles?: StylesBaseVariant; + /** Styles for the element's label. */ labelStyles?: StylesBaseVariant; + /** Styles for the element's error text. */ errorTextStyles?: StylesBaseVariant; + /** Redaction type of the revealed data. */ redaction?: RedactionType } @@ -129,6 +167,9 @@ export interface IRevealResponseType { errors?: Record[]; } +/** + * Supported validation rule types. + */ export enum ValidationRuleType { REGEX_MATCH_RULE = 'REGEX_MATCH_RULE', LENGTH_MATCH_RULE = 'LENGTH_MATCH_RULE', @@ -199,6 +240,9 @@ export const REQUIRED_MARK_DEFAULT_STYLE = { export const ELEMENT_REQUIRED_ASTERISK = ' *'; +/** + * Supported redaction types. + */ export enum RedactionType { DEFAULT = 'DEFAULT', PLAIN_TEXT = 'PLAIN_TEXT',