diff --git a/example/src/pages/NumberPad.tsx b/example/src/pages/NumberPad.tsx index 36d3a9dd..1bbf002b 100644 --- a/example/src/pages/NumberPad.tsx +++ b/example/src/pages/NumberPad.tsx @@ -7,38 +7,81 @@ import { IOStyles, VSpacer, NumberPad, - H3 + H3, + CodeInput, + ListItemSwitch, + IOColors } from "@pagopa/io-app-design-system"; +import { useNavigation } from "@react-navigation/native"; import { Screen } from "../components/Screen"; +const PIN_LENGTH = 6; /** * This Screen is used to test components in isolation while developing. * @returns a screen with a flexed view where you can test components */ export const NumberPadScreen = () => { const [value, setValue] = React.useState(""); + const [blueBackground, setBlueBackground] = React.useState(false); + + const navigation = useNavigation(); + + const onValueChange = (v: string) => { + if (v.length <= PIN_LENGTH) { + setValue(v); + } + }; + + React.useEffect(() => { + navigation.setOptions({ + headerStyle: { + backgroundColor: blueBackground + ? IOColors["blueIO-500"] + : IOColors.white + } + }); + }, [blueBackground, navigation]); return ( - - -

NumberPad

+ + + setBlueBackground(v => !v)} + /> +

NumberPad + Code Input

{"Value Typed on the NumberPad component"}
-

{value}

+

{value}

+ v === "123456"} + /> + Alert.alert("biometric")} /> -
-
+ + ); }; diff --git a/example/src/pages/Sandbox.tsx b/example/src/pages/Sandbox.tsx index 4dcefd6d..d1afa6d0 100644 --- a/example/src/pages/Sandbox.tsx +++ b/example/src/pages/Sandbox.tsx @@ -16,7 +16,13 @@ import { Screen } from "../components/Screen"; export const Sandbox = () => (

Sandbox

{"Insert here the component you're willing to test"}
diff --git a/src/components/codeInput/CodeInput.tsx b/src/components/codeInput/CodeInput.tsx new file mode 100644 index 00000000..a4358080 --- /dev/null +++ b/src/components/codeInput/CodeInput.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useMemo } from "react"; +import { StyleSheet, View } from "react-native"; +import Animated, { + Easing, + useAnimatedStyle, + useSharedValue, + withSequence, + withTiming +} from "react-native-reanimated"; +import { IOColors, IOStyles } from "../../core"; +import { triggerHaptic } from "../../functions"; + +type CodeInputProps = { + value: string; + onValueChange: (value: string) => void; + length: number; + onValidate: (value: string) => boolean; + variant?: "light" | "dark"; +}; + +const DOT_SIZE = 16; + +const styles = StyleSheet.create({ + dotShape: { + width: DOT_SIZE, + height: DOT_SIZE, + borderRadius: 8, + borderWidth: 2 + }, + dotEmpty: { + borderColor: IOColors["grey-200"] + }, + wrapper: { justifyContent: "center", gap: DOT_SIZE } +}); + +const EmptyDot = () => ; + +const FilletDot = ({ color }: { color: IOColors }) => ( + +); + +export const CodeInput = ({ + length, + value, + onValueChange, + variant = "light", + onValidate +}: CodeInputProps) => { + const [status, setStatus] = React.useState<"default" | "error">("default"); + + const translate = useSharedValue(0); + const shakeOffset: number = 8; + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: translate.value }] + })); + + const fillColor = useMemo( + () => + status === "error" + ? "error-600" + : variant === "light" + ? "white" + : "black", + [variant, status] + ); + + useEffect(() => { + if (onValidate && value.length === length) { + const isValid = onValidate(value); + + if (!isValid) { + setStatus("error"); + triggerHaptic("notificationError"); + + // eslint-disable-next-line functional/immutable-data + translate.value = withSequence( + withTiming(shakeOffset, { + duration: 75, + easing: Easing.inOut(Easing.cubic) + }), + withTiming(-shakeOffset, { + duration: 75, + easing: Easing.inOut(Easing.cubic) + }), + withTiming(shakeOffset / 2, { + duration: 75, + easing: Easing.inOut(Easing.cubic) + }), + withTiming(-shakeOffset / 2, { + duration: 75, + easing: Easing.inOut(Easing.cubic) + }), + withTiming(0, { duration: 75, easing: Easing.inOut(Easing.cubic) }) + ); + + const timer = setTimeout(() => { + setStatus("default"); + onValueChange(""); + }, 500); + return () => clearTimeout(timer); + } + } + return; + }, [value, onValidate, length, onValueChange, translate]); + + return ( + + {[...Array(length)].map((_, i) => ( + + {value[i] ? : } + + ))} + + ); +}; diff --git a/src/components/codeInput/index.tsx b/src/components/codeInput/index.tsx new file mode 100644 index 00000000..5766f9e2 --- /dev/null +++ b/src/components/codeInput/index.tsx @@ -0,0 +1 @@ +export * from "./CodeInput"; diff --git a/src/components/index.tsx b/src/components/index.tsx index 32e3de95..4bb45465 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -6,6 +6,7 @@ export * from "./banner"; export * from "./buttons"; export * from "./checkbox"; export * from "./contentWrapper"; +export * from "./codeInput"; export * from "./divider"; export * from "./featureInfo"; export * from "./icons"; diff --git a/src/components/numberpad/NumberPad.tsx b/src/components/numberpad/NumberPad.tsx index 525d183a..ed16552c 100644 --- a/src/components/numberpad/NumberPad.tsx +++ b/src/components/numberpad/NumberPad.tsx @@ -1,5 +1,4 @@ -/* eslint-disable functional/immutable-data */ -import React, { ComponentProps, useRef } from "react"; +import React, { ComponentProps } from "react"; import { View } from "react-native"; import { BiometricsValidType } from "../../utils/types"; import { IONumberPadButtonStyles, IOStyles } from "../../core"; @@ -21,6 +20,7 @@ type BiometricAuthProps = }; type NumberPadProps = { + value: string; onValueChange: (value: string) => void; variant: ComponentProps["variant"]; deleteAccessibilityLabel: string; @@ -47,6 +47,7 @@ const ButtonWrapper = ({ children }: { children: React.ReactNode }) => ( ); export const NumberPad = ({ + value, variant = "dark", onValueChange, biometricType, @@ -54,16 +55,14 @@ export const NumberPad = ({ biometricAccessibilityLabel, deleteAccessibilityLabel }: NumberPadProps) => { - const numberPadValue = useRef(""); - const numberPadPress = (number: number) => { - numberPadValue.current = `${numberPadValue.current}${number}`; - onValueChange(numberPadValue.current); + const newValue = `${value}${number}`; + onValueChange(newValue); }; const onDeletePress = () => { - numberPadValue.current = numberPadValue.current.slice(0, -1); - onValueChange(numberPadValue.current); + const newValue = value.slice(0, -1); + onValueChange(newValue); }; type ButtonType = "biometric" | "delete";