Skip to content

Paul/nicer multiout #5572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Unreleased (develop)

- added: Add eCash.
- changed: Auto launch QR scanner for multi-out payments if previously used
- changed: Auto populate amount for multi-out payments if prior amounts are similar
- changed: Auto scroll to end of `SendScene2` when addresses or amounts change

## 4.28.0 (staging)

Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ exports[`SendScene2 1 spendTarget 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -1605,6 +1606,7 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -3229,6 +3231,7 @@ exports[`SendScene2 2 spendTargets 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -4811,6 +4814,7 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -6052,6 +6056,7 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -7269,6 +7274,7 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -8304,6 +8310,7 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -9661,6 +9668,7 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -10976,6 +10984,7 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down Expand Up @@ -12225,6 +12234,7 @@ exports[`SendScene2 Render SendScene 1`] = `
extraScrollHeight={62}
getScrollResponder={[Function]}
handleOnScroll={[Function]}
innerRef={[Function]}
keyboardDismissMode="interactive"
keyboardOpeningTime={250}
keyboardSpace={0}
Expand Down
60 changes: 52 additions & 8 deletions src/components/scenes/SendScene2.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { add, div, gte, lt, mul } from 'biggystring'
import { abs, add, div, gte, lt, lte, mul, sub } from 'biggystring'
import {
asMaybeInsufficientFundsError,
asMaybeNoAmountSpecifiedError,
Expand Down Expand Up @@ -62,13 +62,15 @@ import { ExchangedFlipInputAmounts, ExchangeFlipInputFields } from '../themed/Ex
import { PinDots } from '../themed/PinDots'
import { SafeSlider } from '../themed/SafeSlider'
import { SelectFioAddress2 } from '../themed/SelectFioAddress2'
import { AddressTile2, ChangeAddressResult } from '../tiles/AddressTile2'
import { AddressEntryMethod, AddressTile2, ChangeAddressResult } from '../tiles/AddressTile2'
import { CountdownTile } from '../tiles/CountdownTile'
import { EditableAmountTile } from '../tiles/EditableAmountTile'
import { ErrorTile } from '../tiles/ErrorTile'

// TODO: Check contentPadding

const SCROLL_TO_END_DELAY_MS = 150

interface Props extends EdgeAppSceneProps<'send2'> {}

export interface SendScene2Params {
Expand Down Expand Up @@ -110,12 +112,15 @@ interface FioSenderInfo {
skipRecord?: boolean
}

// TODO: For now, do not allow multiple targets to be added via GUI. UX is very poor until
// animation is added. Waiting for reanimated v3 which fixes crashes in Layout animations.
// Note: multiple targets can be added via JSON payment protocol to fix payments to Anypay
// invoices.
const ALLOW_MULTIPLE_TARGETS = true

/**
* If the prior two spend targets of a multi-out payment have the same amount
* within 0.5%, then use the same amount for the new spend target.
* This makes it MUCH easier to load many gift cards without having to enter
* amounts manually.
*/
const MULTI_OUT_DIFF_PERCENT = '0.005'
const PIN_MAX_LENGTH = 4
const INFINITY_STRING = '999999999999999999999999999999999999999'

Expand All @@ -125,7 +130,9 @@ const SendComponent = (props: Props) => {
const theme = useTheme()
const styles = getStyles(theme)

const needsScrollToEnd = React.useRef<boolean>(false)
const makeSpendCounter = React.useRef<number>(0)
const scrollViewRef = React.useRef<KeyboardAwareScrollView | null>(null)

const initialMount = React.useRef<boolean>(true)
const pinInputRef = React.useRef<TextInput>(null)
Expand Down Expand Up @@ -161,6 +168,7 @@ const SendComponent = (props: Props) => {
const [edgeTransaction, setEdgeTransaction] = useState<EdgeTransaction | null>(null)
const [pinValue, setPinValue] = useState<string | undefined>(undefined)
const [spendingLimitExceeded, setSpendingLimitExceeded] = useState<boolean>(false)
const [lastAddressEntryMethod, setLastAddressEntryMethod] = useState<AddressEntryMethod | undefined>(undefined)
const [fioSender, setFioSender] = useState<FioSenderInfo>({
fioAddress: fioPendingRequest?.payer_fio_address ?? '',
fioWallet: null,
Expand Down Expand Up @@ -232,7 +240,7 @@ const SendComponent = (props: Props) => {
const handleChangeAddress =
(spendTarget: EdgeSpendTarget) =>
async (changeAddressResult: ChangeAddressResult): Promise<void> => {
const { parsedUri, fioAddress } = changeAddressResult
const { addressEntryMethod, parsedUri, fioAddress } = changeAddressResult

if (parsedUri != null) {
if (parsedUri.metadata != null) {
Expand All @@ -241,21 +249,38 @@ const SendComponent = (props: Props) => {
spendTarget.uniqueIdentifier = parsedUri?.uniqueIdentifier
spendTarget.publicAddress = parsedUri?.publicAddress
spendTarget.nativeAmount = parsedUri?.nativeAmount

if (spendInfo.spendTargets.length > 2 && spendTarget.nativeAmount == null) {
// Check if the last two spend targets have the same amount within 0.5%
const prevAmount = spendInfo.spendTargets[spendInfo.spendTargets.length - 2].nativeAmount
const pprevAmount = spendInfo.spendTargets[spendInfo.spendTargets.length - 3].nativeAmount

if (prevAmount != null && pprevAmount != null) {
const diff = abs(sub(prevAmount, pprevAmount))
const diffPercent = div(diff, prevAmount, DECIMAL_PRECISION)
if (lte(diffPercent, MULTI_OUT_DIFF_PERCENT)) {
spendTarget.nativeAmount = prevAmount
}
}
}
spendTarget.otherParams = {
fioAddress
}

// We can assume the spendTarget object came from the Component spendInfo so simply resetting the spendInfo
// should properly re-render with new spendTargets
setLastAddressEntryMethod(addressEntryMethod)
setMinNativeAmount(parsedUri.minNativeAmount)
setExpireDate(parsedUri?.expireDate)
setSpendInfo({ ...spendInfo })
needsScrollToEnd.current = true
}
}

const handleAddressAmountPress = (index: number) => () => {
spendInfo.spendTargets.splice(index, 1)
setSpendInfo({ ...spendInfo })
needsScrollToEnd.current = true
}

const renderAddressAmountTile = (index: number, spendTarget: EdgeSpendTarget) => {
Expand Down Expand Up @@ -300,7 +325,7 @@ const SendComponent = (props: Props) => {
const { publicAddress = '', otherParams = {} } = spendTarget
const { fioAddress } = otherParams
const title = lstrings.send_scene_send_to_address + (spendInfo.spendTargets.length > 1 ? ` ${(index + 1).toString()}` : '')
const doOpenCamera = openCameraRef.current
const doOpenCamera = openCameraRef.current || (publicAddress === '' && lastAddressEntryMethod === 'scan')
if (openCameraRef.current) openCameraRef.current = false

return (
Expand Down Expand Up @@ -332,6 +357,7 @@ const SendComponent = (props: Props) => {
setSpendInfo({ ...spendInfo })
setMaxSpendSetter(-1)
setFieldChanged(newField)
needsScrollToEnd.current = true
}

const handleSetMax = (index: number) => () => {
Expand Down Expand Up @@ -457,6 +483,7 @@ const SendComponent = (props: Props) => {
const handleAddAddress = useHandler(() => {
spendInfo.spendTargets.push({})
setSpendInfo({ ...spendInfo })
needsScrollToEnd.current = true
})

const renderAddAddress = () => {
Expand Down Expand Up @@ -1114,6 +1141,19 @@ const SendComponent = (props: Props) => {
backgroundColors[0] = scaledColor
}

React.useEffect(() => {
// Hack: While you would think to use InteractionManager.runAfterInteractions,
// it doesn't work because several renders occur before the full height is
// determined and the scrollToEnd call would be effective.
const timeout = setTimeout(() => {
if (needsScrollToEnd.current) {
scrollViewRef.current?.scrollToEnd(true)
needsScrollToEnd.current = false
}
}, SCROLL_TO_END_DELAY_MS)
return () => clearTimeout(timeout)
})

return (
<SceneWrapper
hasNotifications
Expand All @@ -1127,6 +1167,10 @@ const SendComponent = (props: Props) => {
{({ insetStyle }) => (
<>
<StyledKeyboardAwareScrollView
innerRef={ref => {
const kbRef: KeyboardAwareScrollView | null = ref as any
scrollViewRef.current = kbRef
}}
contentContainerStyle={{
...insetStyle,
paddingTop: 0,
Expand Down
17 changes: 10 additions & 7 deletions src/components/tiles/AddressTile2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ import { Airship, showError, showToast } from '../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../services/ThemeContext'
import { EdgeText } from '../themed/EdgeText'

export type AddressEntryMethod = 'scan' | 'other'

export interface ChangeAddressResult {
fioAddress?: string
parsedUri?: EdgeParsedUri
addressEntryMethod: AddressEntryMethod
}

export interface AddressTileRef {
Expand Down Expand Up @@ -90,7 +93,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
// Handlers
// ---------------------------------------------------------------------------

const changeAddress = useHandler(async (address: string) => {
const changeAddress = useHandler(async (address: string, addressEntryMethod: AddressEntryMethod) => {
if (address == null || address === '') return

setLoading(true)
Expand Down Expand Up @@ -164,7 +167,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
}

// set address
await onChangeAddress({ fioAddress, parsedUri })
await onChangeAddress({ fioAddress, parsedUri, addressEntryMethod })
} catch (e: any) {
const currencyInfo = coreWallet.currencyInfo
const ercTokenStandard = currencyInfo.defaultSettings?.otherSettings?.ercTokenStandard ?? ''
Expand Down Expand Up @@ -196,7 +199,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
try {
// Will throw in case uri is invalid
await coreWallet.parseUri(clipboard, currencyCode)
await changeAddress(clipboard)
await changeAddress(clipboard, 'other')
} catch (error) {
showError(error, { trackError: false })
}
Expand All @@ -216,7 +219,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
))
.then(async (result: string | undefined) => {
if (result) {
await changeAddress(result)
await changeAddress(result, 'scan')
}
})
.catch(error => {
Expand All @@ -230,7 +233,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
))
.then(async result => {
if (result) {
await changeAddress(result)
await changeAddress(result, 'other')
}
})
.catch(error => {
Expand Down Expand Up @@ -263,7 +266,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded
// Prefer segwit address if the selected wallet has one
const { segwitAddress, publicAddress } = await wallet.getReceiveAddress({ tokenId: null })
const address = segwitAddress != null ? segwitAddress : publicAddress
await changeAddress(address)
await changeAddress(address, 'other')
})
.catch(err => showError(err))
})
Expand All @@ -282,7 +285,7 @@ export const AddressTile2 = React.forwardRef((props: Props, ref: React.Forwarded

React.useImperativeHandle(ref, () => ({
async onChangeAddress(address: string) {
await changeAddress(address)
await changeAddress(address, 'other')
}
}))

Expand Down
Loading