diff --git a/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.stories.tsx b/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.stories.tsx new file mode 100644 index 0000000000..ccc90af453 --- /dev/null +++ b/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.stories.tsx @@ -0,0 +1,42 @@ +import { Alert, Typography } from '@mui/material' +import type { Meta, StoryFn } from '@storybook/react' +import { FlexCol } from '@xylabs/react-flexbox' +import React, { useState } from 'react' + +import { AccessCodeGateFlexbox } from './AccessCodeGateFlexbox.tsx' + +export default { + component: AccessCodeGateFlexbox, + title: 'access/AccessCodeGateFlexbox', +} as Meta + +const Template: StoryFn = args => ( + +) + +const TemplateWithAccessCodes: StoryFn = (args) => { + const validAccessCodes = ['100519'] + const [validated, setValidated] = useState(false) + const onAccessCodeSuccess = () => { + setValidated(true) + } + const validateFunction = (code?: string) => code?.length === 6 + + return validated + ? Success! + : ( + + + Hint: 100519 + + ) +} + +const Default = Template.bind({}) +const WithAccessCodes = TemplateWithAccessCodes.bind({}) + +export { Default, WithAccessCodes } diff --git a/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.tsx b/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.tsx new file mode 100644 index 0000000000..dee766eb31 --- /dev/null +++ b/packages/sdk/packages/access-gate/src/components/AccessCodeGateFlexbox.tsx @@ -0,0 +1,99 @@ +/* eslint-disable @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */ +import { FormControl } from '@mui/material' +import { ButtonEx } from '@xylabs/react-button' +import type { FlexBoxProps } from '@xylabs/react-flexbox' +import { FlexGrowCol, FlexGrowRow } from '@xylabs/react-flexbox' +import type { WithChildren } from '@xylabs/react-shared' +import React, { + useCallback, useEffect, useState, +} from 'react' + +import { CodeTextField } from './CodeTextField.tsx' + +export interface AccessCodeGateFlexbox extends WithChildren, FlexBoxProps { + onAccessCodeSuccess?: (code?: string) => void + textFieldHelperText?: string + userAccessCodes?: string[] + validAccessCodes?: string[] + validateFunction?: (codeInput?: string) => boolean +} + +export const AccessCodeGateFlexbox: React.FC = ({ + children, + onAccessCodeSuccess, + userAccessCodes, + validAccessCodes, + validateFunction, + ...props +}) => { + const [initialized, setInitialized] = useState(false) + const [accessGranted, setAccessGranted] = useState(false) + const [codeInput, setCodeInput] = useState() + const [validCode, setValidCode] = useState(null) + + const disabled = validateFunction ? !validateFunction(codeInput) : !codeInput + const validateCode = useCallback((accessCode: string) => (accessCode ? validAccessCodes?.includes(accessCode) : false), [validAccessCodes]) + + const onEnter = () => { + if (codeInput) { + const granted = validateCode(codeInput) + if (granted) { + setValidCode(true) + // delay success callback to ensure the ui shows success before next action + setTimeout(() => { + setAccessGranted(granted) + onAccessCodeSuccess?.(codeInput) + }, 1500) + } else { + setValidCode(false) + } + } + } + + useEffect(() => { + // whenever a code changes, reset the success/failure warning + setValidCode(null) + }, [codeInput]) + + useEffect(() => { + if (userAccessCodes) { + const granted = userAccessCodes.some(code => validateCode(code)) + setAccessGranted(granted) + if (granted) { + onAccessCodeSuccess?.() + } + } + setInitialized(true) + }, [onAccessCodeSuccess, userAccessCodes, validateCode]) + + return ( + <> + {initialized + ? accessGranted + ? children + : ( + + + + + + + + Enter + + + + + ) + + : null} + + ) +} diff --git a/packages/sdk/packages/access-gate/src/components/CodeTextField.tsx b/packages/sdk/packages/access-gate/src/components/CodeTextField.tsx new file mode 100644 index 0000000000..888053f40d --- /dev/null +++ b/packages/sdk/packages/access-gate/src/components/CodeTextField.tsx @@ -0,0 +1,45 @@ +import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material' +import type { TextFieldProps } from '@mui/material' +import { + InputAdornment, styled, TextField, +} from '@mui/material' +import type { Dispatch, SetStateAction } from 'react' +import React from 'react' + +export type CodeTextFieldProps = TextFieldProps & { + codeInput?: string + disabled?: boolean + onEnter?: () => void + setCodeInput?: Dispatch> + validCode?: boolean | null +} + +export const CodeTextField: React.FC = ({ + codeInput, disabled, onEnter, setCodeInput, validCode, ...props +}) => ( + + {/* Having a display block element for all 3 states (null, false, true) means the icon coming in and out + does not affect the overall width */} + + + + + ), + }} + onKeyUp={event => (event.key === 'Enter' && !disabled ? onEnter?.() : null)} + autoFocus + size="small" + value={codeInput ?? ''} + onChange={event => setCodeInput?.(event.target.value)} + {...props} + /> +) + +const StyledTextField = styled(TextField, { name: 'StyledTextField' })(() => ({ '& .MuiInputBase-root': { paddingRight: 0 } })) diff --git a/packages/sdk/packages/access-gate/src/components/index.ts b/packages/sdk/packages/access-gate/src/components/index.ts index 336ce12bb9..08dd45ed50 100644 --- a/packages/sdk/packages/access-gate/src/components/index.ts +++ b/packages/sdk/packages/access-gate/src/components/index.ts @@ -1 +1 @@ -export {} +export * from './AccessCodeGateFlexbox.tsx' diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index a557c890ca..ca7d223b7e 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,3 +1,4 @@ +export * from '@xyo-network/react-access-gate' export * from '@xyo-network/react-address' export * from '@xyo-network/react-app-settings' export * from '@xyo-network/react-appbar'