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";