0.4.0 โ€ข Published 11 months ago

input-otp-native v0.4.0

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

Input OTP Native

Input OTP Native ๐Ÿ”

One time passcode Input For React Native/Expo. Unstyled and fully customizable.

Features

  • ๐Ÿ“ฑ Built specifically for React Native/Expo
  • ๐ŸŽจ Fully customizable styling with render props ( supports nativewind )
  • ๐Ÿ“‹ Four copy paste styles (Apple, Stripe, Revolt, Dashed)
  • ๐Ÿงช 100% test coverage

Installation

## npm
npm install input-otp-native

## yarn
yarn add input-otp-native

#pnpm
pnpm add input-otp-native

Examples

We create a few examples that you can copy paste and use in your project.

๐Ÿ’ณ Stripe OTP Input with Nativewind

import { View, Text } from 'react-native';
import { OTPInput, type SlotProps } from 'input-otp-native';
import type { OTPInputRef } from 'input-otp-native';
import { useRef } from 'react';
import { Alert } from 'react-native';

import Animated, {
  useAnimatedStyle,
  withRepeat,
  withTiming,
  withSequence,
  useSharedValue,
} from 'react-native-reanimated';
import { useEffect } from 'react';
import { cn } from './utils';

export default function StripeOTPInput() {
  const ref = useRef<OTPInputRef>(null);
  const onComplete = (code: string) => {
    Alert.alert('Completed with code:', code);
    ref.current?.clear();
  };

  return (
    <OTPInput
      ref={ref}
      onComplete={onComplete}
      maxLength={6}
      render={({ slots }) => (
        <View className="flex-1 flex-row items-center justify-center my-4">
          <View className="flex-row">
            {slots.slice(0, 3).map((slot, idx) => (
              <Slot key={idx} {...slot} index={idx} />
            ))}
          </View>
          <FakeDash />
          <View className="flex-row">
            {slots.slice(3).map((slot, idx) => (
              <Slot key={idx} {...slot} index={idx} />
            ))}
          </View>
        </View>
      )}
    />
  );
}

function Slot({
  char,
  isActive,
  hasFakeCaret,
  index,
}: SlotProps & { index: number }) {
  const isFirst = index === 0;
  const isLast = index === 2;
  return (
    <View
      className={cn(
        `w-12 h-16 items-center justify-center bg-gray-50`,
        'border border-gray-200',
        {
          'rounded-r-lg': isLast,
          'rounded-l-lg': isFirst,
          'bg-white border-black': isActive,
        }
      )}
    >
      {char !== null && (
        <Text className="text-2xl font-medium text-gray-900">{char}</Text>
      )}
      {hasFakeCaret && <FakeCaret />}
    </View>
  );
}

function FakeDash() {
  return (
    <View className="w-8 items-center justify-center">
      <View className="w-2 h-0.5 bg-gray-200 rounded-sm" />
    </View>
  );
}

function FakeCaret() {
  const opacity = useSharedValue(1);

  useEffect(() => {
    opacity.value = withRepeat(
      withSequence(
        withTiming(0, { duration: 500 }),
        withTiming(1, { duration: 500 })
      ),
      -1,
      true
    );
  }, [opacity]);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));

  const baseStyle = {
    width: 2,
    height: 32,
    backgroundColor: 'black',
    borderRadius: 1,
  };

  return (
    <View className="absolute w-full h-full items-center justify-center">
      <Animated.View style={[baseStyle, animatedStyle]} />
    </View>
  );
}

๐Ÿ’ณ Stripe OTP Input

import { View, Text, StyleSheet, type ViewStyle, Alert } from 'react-native';
import { OTPInput, type SlotProps } from 'input-otp-native';
import type { OTPInputRef } from 'input-otp-native';
import { useRef } from 'react';

import Animated, {
  useAnimatedStyle,
  withRepeat,
  withTiming,
  withSequence,
  useSharedValue,
} from 'react-native-reanimated';
import { useEffect } from 'react';

export default function StripeOTPInput() {
  const ref = useRef<OTPInputRef>(null);
  const onComplete = (code: string) => {
    Alert.alert('Completed with code:', code);
    ref.current?.clear();
  };

  return (
    <OTPInput
      ref={ref}
      onComplete={onComplete}
      maxLength={6}
      render={({ slots }) => (
        <View style={styles.mainContainer}>
          <View style={styles.slotsContainer}>
            {slots.slice(0, 3).map((slot, idx) => (
              <Slot key={idx} {...slot} index={idx} />
            ))}
          </View>
          <FakeDash />
          <View style={styles.slotsContainer}>
            {slots.slice(3).map((slot, idx) => (
              <Slot key={idx} {...slot} index={idx} />
            ))}
          </View>
        </View>
      )}
    />
  );
}

function Slot({
  char,
  isActive,
  hasFakeCaret,
  index,
}: SlotProps & { index: number }) {
  const isFirst = index === 0;
  const isLast = index === 2;

  return (
    <View
      style={[
        styles.slot,
        isFirst && styles.slotFirst,
        isLast && styles.slotLast,
        isActive && styles.activeSlot,
      ]}
    >
      {char !== null && <Text style={styles.char}>{char}</Text>}
      {hasFakeCaret && <FakeCaret />}
    </View>
  );
}

function FakeDash() {
  return (
    <View style={styles.fakeDashContainer}>
      <View style={styles.fakeDash} />
    </View>
  );
}

function FakeCaret({ style }: { style?: ViewStyle }) {
  const opacity = useSharedValue(1);

  useEffect(() => {
    opacity.value = withRepeat(
      withSequence(
        withTiming(0, { duration: 500 }),
        withTiming(1, { duration: 500 })
      ),
      -1,
      true
    );
  }, [opacity]);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));

  return (
    <View style={styles.fakeCaretContainer}>
      <Animated.View style={[styles.fakeCaret, style, animatedStyle]} />
    </View>
  );
}

const styles = StyleSheet.create({
  mainContainer: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginVertical: 16,
  },
  slotsContainer: {
    flexDirection: 'row',
  },
  slot: {
    width: 42,
    height: 52,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#F9FAFB',
    borderWidth: 1,
    borderColor: '#E5E7EB',
  },
  slotFirst: {
    borderTopLeftRadius: 8,
    borderBottomLeftRadius: 8,
  },
  slotLast: {
    borderTopRightRadius: 8,
    borderBottomRightRadius: 8,
  },
  activeSlot: {
    backgroundColor: '#FFF',
    borderColor: '#000',
  },
  char: {
    fontSize: 22,
    fontWeight: '500',
    color: '#111827',
  },
  fakeDashContainer: {
    width: 32,
    alignItems: 'center',
    justifyContent: 'center',
  },
  fakeDash: {
    width: 8,
    height: 2,
    backgroundColor: '#E5E7EB',
    borderRadius: 1,
  },
  /* Caret */
  fakeCaretContainer: {
    position: 'absolute',
    width: '100%',
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
  },
  fakeCaret: {
    width: 2,
    height: 32,
    backgroundColor: '#000',
    borderRadius: 1,
  },
});

๐Ÿ Apple OTP Input with Nativewind

๐ŸŽ Apple OTP Input

๐Ÿ”„ Revolt OTP Input

๐Ÿ”„ Revolt OTP Input with Nativewind

ใ€ฐ๏ธ Dashed OTP Input

ใ€ฐ๏ธ Dashed OTP Input with Nativewind

API Reference

OTPInput Props

PropTypeDefaultDescription
maxLengthnumberRequiredNumber of OTP digits
render(props: RenderProps) => ReactNodeRequiredRender function for OTP slots
valuestringundefinedControlled value of the input
onChange(value: string) => voidundefinedCallback when value changes
onComplete(value: string) => voidundefinedCallback when all digits are filled
containerStyleViewStyleundefinedStyle for the container
patternstringundefinedRegex pattern for input validation
textAlign'left' | 'center' | 'right''left'Text alignment within input
pasteTransformer(pasted: string) => stringundefinedTransform pasted text

RenderProps

PropTypeDescription
slotsSlotProps[]Array of slot objects to render
isFocusedbooleanWhether the input is focused

SlotProps

PropTypeDescription
charstring | nullCharacter in the slot
isActivebooleanWhether the slot is active
hasFakeCaretbooleanWhether to show fake caret
placeholderCharstring | nullPlaceholder character

Web support

The library is mainly inspired by otp-input and has a similar API, so we recommend using it on the web.

We can easily create the same component for web and create a new file for it (example/src/examples/apple.web.tsx)

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT


Credits

0.4.0

11 months ago

0.3.0

12 months ago

0.2.0

12 months ago