QuickPay Interaction
A polished quick-pay interaction with avatar carousel, haptic slider, and animated transaction states.
Gesture-driven bottom sheet
The pay sheet springs open from a natural drag gesture and dismisses by swiping down with full velocity-aware physics.
Snapping avatar carousel
A horizontal avatar rail that snaps to the nearest recipient, with a subtle scale pulse on the active selection.
Fully controlled tick-slider
An amount slider driven entirely by gesture position, emitting live haptic ticks at each denomination step.
Race-safe async sends
The send action guards against double-submission and resolves to an animated success state only once the transaction settles.
Installation
Copy the command directly into your project.
Install the needed dependencies
Use in your app
Built to match the preview exactly — for the complete implementation, check the repo on GitHub.
import { useState } from 'react'
import { StatusBar, StyleSheet, Text, TouchableOpacity } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context'
import QuickPayCard from '../../components/quick-pay/components/Quickpay'
import { Contact } from '../../components/quick-pay/constants/types'
const DEMO_CONTACTS: Contact[] = [
{ id: '1', avatar: require('../../components/quick-pay/assets/images/manas2.png'), handle: '@ProyaX' },
{ id: '2', avatar: require('../../components/quick-pay/assets/images/billy.png'), handle: '@RahultDev' },
{ id: '3', avatar: require('../../components/quick-pay/assets/images/boy.png'), handle: '@ManasCodeX' },
]
export default function Index() {
const [cardVisible, setCardVisible] = useState(false)
return (
<GestureHandlerRootView style={styles.root}>
<SafeAreaView style={styles.screen}>
<StatusBar barStyle="light-content" />
<Text style={styles.screenTitle}>Quick Pay</Text>
<TouchableOpacity
style={styles.triggerBtn}
activeOpacity={0.8}
onPress={() => setCardVisible(true)}
>
<Text style={styles.triggerText}>Send Money</Text>
</TouchableOpacity>
<QuickPayCard
visible={cardVisible}
onClose={() => setCardVisible(false)}
contacts={DEMO_CONTACTS}
onConfirm={async (amount, contact) => {
await new Promise((resolve) => setTimeout(resolve, 2500))
console.log(`Sent ${amount} to ${contact.handle}`)
}}
/>
</SafeAreaView>
</GestureHandlerRootView>
)
}
const styles = StyleSheet.create({
root: {
flex: 1,
},
screen: {
flex: 1,
backgroundColor: '#0a0a0a',
alignItems: 'center',
justifyContent: 'center',
gap: 24,
},
screenTitle: {
color: '#fff',
fontSize: 22,
fontWeight: '600',
letterSpacing: -0.3,
},
triggerBtn: {
backgroundColor: '#fff',
paddingHorizontal: 40,
paddingVertical: 16,
borderRadius: 50,
},
triggerText: {
color: '#111',
fontSize: 16,
fontWeight: '600',
},
})API
<QuickPayCard>
| Prop | Type | Default | Description |
|---|---|---|---|
| visible | boolean | — | Controls open/close. Animates in on true, animates out and unmounts on false. |
| onClose | () => void | — | Called on backdrop tap, swipe-to-dismiss, or after a successful send is acknowledged. |
| contacts | readonly Contact[] | — | Contacts shown in the avatar carousel. Must be non-empty. |
| initialAmount | number | 1000 | Starting amount, clamped to [minAmount, maxAmount]. |
| onConfirm | (amount: number, contact: Contact) => void | Promise<void> | — | Called on Confirm tap. If it returns a promise, the card waits for it before showing the sent state. |
| onSendError | (error: unknown) => void | — | Called if the onConfirm promise rejects; the card resets to the form stage. |
| minAmount | number | 100 | Minimum selectable amount. |
| maxAmount | number | 1800 | Maximum selectable amount. |
| step | number | 100 | Increment between tick marks. |
| backgroundSource | LottieSource | built-in subtle background | Background Lottie animation behind the card content. |
| sendingIconSource | FlipIconSource | built-in plane icon | Front/back frames for the flip animation during sending. |
| sentIconSource | FlipIconSource | built-in checkmark icon | Front/back frames for the flip animation during sent. |
<AvatarCarousel>
| Prop | Type | Default | Description |
|---|---|---|---|
| contacts | readonly Contact[] | — | Must be non-empty. |
| initialIndex | number | 3 | Index centered on mount. Clamped internally if contacts.length is smaller. Fires onContactChange once on mount with whichever contact ends up centered. |
| onContactChange | (contact: Contact) => void | — | Fires on mount and on every scroll snap. |
| height | number | 120 | Height of the carousel track. |
<TickSlider>
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | Fully controlled. External changes move the thumb, except mid-drag. |
| onValueChange | (value: number) => void | — | Fires on every snap while dragging. |
| min | number | 100 | Minimum value. |
| max | number | 1800 | Maximum value. |
| step | number | 100 | Increment between ticks. |
<SendStatusCard>
| Prop | Type | Default | Description |
|---|---|---|---|
| stage | 'sending' | 'sent' | — | Which state to render. |
| sendingIconSource | FlipIconSource | built-in plane icon | Front/back frames for the flip animation during sending. |
| sentIconSource | FlipIconSource | built-in checkmark icon | Front/back frames for the flip animation during sent. |
Types
interface Contact {
readonly id: string;
readonly avatar: ImageSourcePropType;
readonly handle: string;
readonly name?: string; // reserved — not rendered by the component today
}
interface FlipIconSource {
readonly front: ImageSourcePropType;
readonly back: ImageSourcePropType;
}