Skip to content

Commit

Permalink
[PAY-1860] Implements Pay Extra form on mobile (#6184)
Browse files Browse the repository at this point in the history
  • Loading branch information
schottra authored Oct 3, 2023
1 parent a7fccae commit cbcac0f
Show file tree
Hide file tree
Showing 21 changed files with 560 additions and 244 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"endpoint":"http://audius-protocol-discovery-provider-1","timestamp":1695756968160}
76 changes: 40 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions packages/common/src/utils/decimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const PRECISION = 2

export type DecimalUtilOptions = {
/** Number of digits used on the right side of human-readable values. Defaults to 2 */
precision?: number
}

/**
* Helper to parse values from text input and give numeric values and human readable values out. Removes characters that are not numbers or decimals.
*/
export const filterDecimalString = (
value: string,
{ precision = PRECISION }: DecimalUtilOptions = {}
) => {
const input = value.replace(/[^0-9.]+/g, '')
// Regex to grab the whole and decimal parts of the number, stripping duplicate '.' characters
const match = input.match(/^(?<whole>\d*)(?<dot>.)?(?<decimal>\d*)/)
const { whole, decimal, dot } = match?.groups || {}

// Conditionally render the decimal part, and only for the number of decimals specified
const stringAmount = dot
? `${whole}.${(decimal ?? '').substring(0, precision)}`
: whole
return { human: stringAmount, value: Number(stringAmount) * 10 ** precision }
}

/**
* Helper to pad a decimal value to a specified precision, useful for blur events on a token input
*/
export const padDecimalValue = (
value: string,
{ precision = PRECISION }: Pick<DecimalUtilOptions, 'precision'> = {}
) => {
const [whole, decimal] = value.split('.')

const paddedDecimal = (decimal ?? '')
.substring(0, precision)
.padEnd(precision, '0')
return `${whole.length > 0 ? whole : '0'}.${paddedDecimal}`
}

/** Converts an integer value representing a decimal number to it's human-readable form. (ex. 100 => 1.00) */
export const decimalIntegerToHumanReadable = (
value: number,
{ precision = PRECISION }: DecimalUtilOptions = {}
) => {
return (value / 10 ** precision).toFixed(precision)
}

/** Converts a string representing a decimal number to its equivalent integer representation (ex. 1.00 => 100) */
export const decimalIntegerFromHumanReadable = (
value: string,
{ precision = PRECISION }: DecimalUtilOptions = {}
) => {
return parseFloat(value) * 10 ** precision
}
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './allSettled'
export * from './browserNotifications'
export * from './collectionUtils'
export * from './decimal'
export * from './error'
export * from './fillString'
export * from './formatUtil'
Expand Down
5 changes: 3 additions & 2 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"deprecated-react-native-prop-types": "4.0.0",
"expo-crypto": "9.2.0",
"ffmpeg-kit-react-native": "4.5.2",
"formik": "2.2.9",
"formik": "2.4.1",
"fxa-common-password-list": "0.0.4",
"jimp": "0.14.0",
"linkifyjs": "4.1.0",
Expand Down Expand Up @@ -170,7 +170,8 @@
"tweetnacl": "1.0.3",
"type-fest": "2.16.0",
"typed-redux-saga": "1.3.1",
"yup": "0.32.11"
"yup": "0.32.11",
"zod-formik-adapter": "1.2.0"
},
"optionalDependencies": {
"ios-deploy": "1.11.4"
Expand Down
41 changes: 35 additions & 6 deletions packages/mobile/src/components/core/HarmonySelectablePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Animated, Pressable, Text } from 'react-native'

import { usePressScaleAnimation } from 'app/hooks/usePressScaleAnimation'
import type { StylesProps } from 'app/styles'
import { makeStyles } from 'app/styles'
import { makeStyles, shadow } from 'app/styles'

const useStyles = makeStyles(({ spacing, palette, typography }) => ({
pill: {
Expand All @@ -21,20 +21,32 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({
borderColor: palette.neutralLight7,
backgroundColor: palette.white
},
pillLarge: {
...shadow()
},
pressable: {
alignItems: 'center',
justifyContent: 'center',
height: spacing(6),
paddingLeft: spacing(3),
paddingRight: spacing(3),
gap: spacing(1)
gap: spacing(1),
width: '100%'
},
pressableLarge: {
height: spacing(8),
paddingLeft: spacing(4),
paddingRight: spacing(4)
},
text: {
fontSize: typography.fontSize.medium,
fontFamily: typography.fontByWeight.medium,
color: palette.neutralLight4,
lineHeight: 1.25 * typography.fontSize.medium
},
textLarge: {
color: palette.neutral
},
pressed: {
backgroundColor: palette.secondaryLight1,
borderColor: palette.secondary
Expand All @@ -54,12 +66,21 @@ type HarmonySelectablePillProps = Omit<ButtonProps, 'title'> &
isSelected: boolean
icon?: React.ReactElement
label: string
size?: 'default' | 'large'
} & StylesProps<{ root: ViewStyle }>

export const HarmonySelectablePill = (props: HarmonySelectablePillProps) => {
const styles = useStyles()
const { isSelected, label, icon, onPressIn, onPressOut, style, ...other } =
props
const {
isSelected,
label,
icon,
onPressIn,
onPressOut,
size,
style,
...other
} = props

const {
scale,
Expand Down Expand Up @@ -87,6 +108,7 @@ export const HarmonySelectablePill = (props: HarmonySelectablePillProps) => {
<Animated.View
style={[
styles.pill,
size === 'large' ? styles.pillLarge : undefined,
isSelected ? styles.pressed : undefined,
{ transform: [{ scale }] },
style
Expand All @@ -96,12 +118,19 @@ export const HarmonySelectablePill = (props: HarmonySelectablePillProps) => {
accessibilityRole='button'
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={[styles.pressable]}
style={[
styles.pressable,
size === 'large' ? styles.pressableLarge : undefined
]}
{...other}
>
{icon || null}
<Text
style={[styles.text, isSelected ? styles.textPressed : undefined]}
style={[
styles.text,
size === 'large' ? styles.textLarge : undefined,
isSelected ? styles.textPressed : undefined
]}
>
{label}
</Text>
Expand Down
74 changes: 74 additions & 0 deletions packages/mobile/src/components/fields/PriceField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState, useEffect, useCallback } from 'react'

import {
decimalIntegerFromHumanReadable,
decimalIntegerToHumanReadable,
filterDecimalString,
padDecimalValue
} from '@audius/common'
import { useField } from 'formik'
import type {
NativeSyntheticEvent,
TextInputChangeEventData,
TextInputFocusEventData
} from 'react-native'

import { Text } from '../core/Text'

import type { TextFieldProps } from './TextField'
import { TextField } from './TextField'

const messages = {
dollars: '$'
}

/** Implements a Formik field for entering a price, including default dollar sign
* adornment and conversion logic to/from human readable price. Internal value is stored
* as an integer number of cents.
*/
export const PriceField = (props: TextFieldProps) => {
const [{ value }, , { setValue: setPrice }] = useField<number>(props.name)
const [humanizedValue, setHumanizedValue] = useState(
value ? decimalIntegerToHumanReadable(value) : null
)

useEffect(() => {
if (humanizedValue !== null) {
const dehumanizedValue = decimalIntegerFromHumanReadable(humanizedValue)
if (value === undefined || dehumanizedValue !== value) {
setPrice(dehumanizedValue)
}
}
}, [value, humanizedValue, setPrice])

const handlePriceChange = useCallback(
(e: NativeSyntheticEvent<TextInputChangeEventData>) => {
const { human, value } = filterDecimalString(e.nativeEvent.text)
setHumanizedValue(human)
setPrice(value)
},
[setPrice, setHumanizedValue]
)

const handlePriceBlur = useCallback(
(e: NativeSyntheticEvent<TextInputFocusEventData>) => {
setHumanizedValue(padDecimalValue(e.nativeEvent.text))
},
[]
)

return (
<TextField
keyboardType='numeric'
startAdornment={
<Text color='neutralLight2' weight='bold'>
{messages.dollars}
</Text>
}
{...props}
value={humanizedValue ?? undefined}
onChange={handlePriceChange}
onBlur={handlePriceBlur}
/>
)
}
Loading

0 comments on commit cbcac0f

Please sign in to comment.