├── .watchmanconfig ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── .gitignore ├── .expo-shared └── assets.json ├── components └── CurrencyInput │ ├── styles.ts │ └── index.tsx ├── tsconfig.json ├── app.json ├── package.json └── App.tsx /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larkintuckerllc/RNCurrencyInput/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larkintuckerllc/RNCurrencyInput/HEAD/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /components/CurrencyInput/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | input: { 5 | opacity: 0, 6 | }, 7 | text: { 8 | marginBottom: 0, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "RNCurrencyInput", 4 | "slug": "RNCurrencyInput", 5 | "privacy": "public", 6 | "sdkVersion": "35.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^35.0.0", 12 | "react": "16.8.3", 13 | "react-dom": "16.8.3", 14 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz", 15 | "react-native-web": "^0.11.7" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^16.8.23", 19 | "@types/react-native": "^0.57.65", 20 | "babel-preset-expo": "^7.0.0", 21 | "typescript": "^3.4.5" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { StyleSheet, Text, TextInput, View } from 'react-native'; 3 | import CurrencyInput from './components/CurrencyInput'; 4 | 5 | export default function App() { 6 | const [value, setValue] = useState(0); 7 | const handleValueChange = useCallback(val => { 8 | // eslint-disable-next-line 9 | console.log(val); 10 | setValue(val); 11 | }, []); 12 | 13 | 14 | return ( 15 | 16 | Open up App.tsx to start working on your app! 17 | 23 | Validating bottom margin. 24 | 25 | ); 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | container: { 30 | flex: 1, 31 | backgroundColor: '#fff', 32 | alignItems: 'center', 33 | justifyContent: 'center', 34 | }, 35 | input: { 36 | backgroundColor: 'white', 37 | borderColor: 'gray', 38 | borderWidth: 1, 39 | fontSize: 20, 40 | marginBottom: 20, 41 | marginTop: 20, 42 | padding: 20, 43 | textAlign: 'right', 44 | width: 300, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /components/CurrencyInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useState } from 'react'; 2 | import { LayoutChangeEvent, StyleProp, StyleSheet, Text, TextStyle, TextInput } from 'react-native'; 3 | import styles from './styles'; 4 | 5 | const VALID = /^[1-9]{1}[0-9]*$/; 6 | 7 | interface Props { 8 | max?: number; 9 | onValueChange: (value: number) => void; 10 | style?: StyleProp; 11 | value: number; 12 | } 13 | 14 | const CurrencyInput: FC = ({ 15 | max = Number.MAX_SAFE_INTEGER, 16 | onValueChange, 17 | style = { marginBottom: 0 }, 18 | value 19 | }) => { 20 | const valueAbsTrunc = Math.trunc(Math.abs(value)); 21 | if (value !== valueAbsTrunc || !Number.isFinite(value) || Number.isNaN(value)) { 22 | throw new Error(`invalid value property`); 23 | } 24 | const [inputHeight, setInputHeight] = useState(0); 25 | const [inputWidth, setInputWidth] = useState(0); 26 | const handleChangeText = useCallback((text: string) => { 27 | if (text === '') { 28 | onValueChange(0); 29 | return; 30 | } 31 | if (!VALID.test(text)) { 32 | return; 33 | } 34 | const nextValue = parseInt(text, 10); 35 | if (nextValue > max) { 36 | return; 37 | } 38 | onValueChange(nextValue); 39 | }, []); 40 | const handleLayout = useCallback((event: LayoutChangeEvent) => { 41 | const { height, width } = event.nativeEvent.layout; 42 | setInputHeight(height); 43 | setInputWidth(width); 44 | }, []); 45 | const valueInput = value === 0 ? '' : value.toString(); 46 | const valueDisplay = (value / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }); 47 | const { marginBottom } = StyleSheet.flatten(style); 48 | 49 | return ( 50 | <> 51 | 52 | {valueDisplay} 53 | 54 | 69 | 70 | ); 71 | }; 72 | 73 | export default CurrencyInput; 74 | --------------------------------------------------------------------------------