ZAYX logo
XGitHubDiscordReddit

QuickPay Interaction

A polished quick-pay interaction with avatar carousel, haptic slider, and animated transaction states.

Open in Github
Open in ChatGPT
Open in Claude
Open in Cursor

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.

npx degit ManasCodeXart/ZAYX/components/quick-pay components/quick-pay

Install the needed dependencies

npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler expo-haptics lottie-react-native

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>

PropTypeDefaultDescription
visiblebooleanControls open/close. Animates in on true, animates out and unmounts on false.
onClose() => voidCalled on backdrop tap, swipe-to-dismiss, or after a successful send is acknowledged.
contactsreadonly Contact[]Contacts shown in the avatar carousel. Must be non-empty.
initialAmountnumber1000Starting 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) => voidCalled if the onConfirm promise rejects; the card resets to the form stage.
minAmountnumber100Minimum selectable amount.
maxAmountnumber1800Maximum selectable amount.
stepnumber100Increment between tick marks.
backgroundSourceLottieSourcebuilt-in subtle backgroundBackground Lottie animation behind the card content.
sendingIconSourceFlipIconSourcebuilt-in plane iconFront/back frames for the flip animation during sending.
sentIconSourceFlipIconSourcebuilt-in checkmark iconFront/back frames for the flip animation during sent.

<AvatarCarousel>

PropTypeDefaultDescription
contactsreadonly Contact[]Must be non-empty.
initialIndexnumber3Index centered on mount. Clamped internally if contacts.length is smaller. Fires onContactChange once on mount with whichever contact ends up centered.
onContactChange(contact: Contact) => voidFires on mount and on every scroll snap.
heightnumber120Height of the carousel track.

<TickSlider>

PropTypeDefaultDescription
valuenumberFully controlled. External changes move the thumb, except mid-drag.
onValueChange(value: number) => voidFires on every snap while dragging.
minnumber100Minimum value.
maxnumber1800Maximum value.
stepnumber100Increment between ticks.

<SendStatusCard>

PropTypeDefaultDescription
stage'sending' | 'sent'Which state to render.
sendingIconSourceFlipIconSourcebuilt-in plane iconFront/back frames for the flip animation during sending.
sentIconSourceFlipIconSourcebuilt-in checkmark iconFront/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; }

Project Structure

quick-pay/
├── components/
│ ├── QuickPayCard.tsx # Root component — gesture-driven bottom sheet
│ ├── AvatarCarousel.tsx # Contact selection carousel
│ ├── TickSlider.tsx # Haptic amount tick slider
│ └── SendStatusCard.tsx # Sending / sent flip-transition states
├── constants/
│ ├── types.ts # Contact, QuickPayStage, FlipIconSource types
│ └── scaling.ts # verticalScale helpers
├── utils/
│ └── format.ts # Amount formatting
└── assets/