ZAYX logo
XGitHubDiscordReddit

Fintech Island

A Dynamic Island–style pill that morphs into a full status tray for sends, card additions, and savings deposits.

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

Dynamic Island–style morph

One pill smoothly resizes into a compact card, then a full detail tray driven by shared-value springs, no remounting, no layout jump.

Three built-in variants

send, cardAdded, and savings each ship with their own Lottie animation, pill label, and expanded-tray content.

Imperative ref API

triggerSuccess() resolves a pending send from 'Sending…' to 'Sent'; dismiss() closes the island from anywhere in your app.

Flippable card carousel

The cardAdded tray's cards flip to reveal a CVV face on tap, with scroll-driven scale and opacity per card.

Installation

Copy the command directly into your project.

npx degit ManasCodeXart/ZAYX/components/fintech-island components/fintech-island

Install the needed dependencies

npx expo install react-native-reanimated react-native-worklets lottie-react-native

Use in your app

Built to match the preview exactly — for the complete implementation, check the repo on GitHub.

import { useCallback, useRef, useState } from 'react' import { Pressable, StyleSheet, Text, View } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SafeAreaView } from 'react-native-safe-area-context' import { FintechIslandProps, FintechIslandRef, RecentTransaction } from '../../components/fintech-island/constants/types' import { FintechIsland } from '../../components/fintech-island/FintechIsland' const MOCK_TRANSACTION = { recipientName: 'Dheeraj Singh', avatarSource: require('../../components/fintech-island/assets/images/roy.png'), recipientHandle: '@solara.vault', currency: '$', amount: 500.0, recentTransactions: [ { id: '1', name: 'Shaaya', handle: '@apexchain', currency: '$', amount: 250.4, avatarSource: require('../../components/fintech-island/assets/images/avatar1.png'), }, { id: '2', name: 'Edward Jane', handle: '@flux', currency: '$', amount: 52.03, avatarSource: require('../../components/fintech-island/assets/images/avatar2.png'), }, { id: '3', name: 'Prateek Rai', handle: '@madara', currency: '$', amount: 180.0, avatarSource: require('../../components/fintech-island/assets/images/avatar3.png'), }, ], } const MOCK_CARD_ADDED = { cardholderName: 'Roy Mustang', cardholderHandle: '@Flameer', cardholderAvatar: require('../../components/fintech-island/assets/images/roy.png'), cardBalance: 8920, currencySymbol: '$', cards: [ { id: '1', lastFour: '0000', holderName: 'Roy Mustang', expiry: '12/24', cvv: '123', backgroundImage: require('../../components/fintech-island/assets/images/bg4.png'), networkLogo: require('../../components/fintech-island/assets/images/networkLogo.png'), paypassImage: require('../../components/fintech-island/assets/images/paypass.png'), }, { id: '2', lastFour: '8821', holderName: 'Roy Mustang', expiry: '06/27', cvv: '225', backgroundImage: require('../../components/fintech-island/assets/images/bg3.png'), networkLogo: require('../../components/fintech-island/assets/images/networkLogo.png'), paypassImage: require('../../components/fintech-island/assets/images/paypass.png'), }, { id: '3', lastFour: '4321', holderName: 'Roy Mustang', expiry: '02/28', cvv: '295', backgroundImage: require('../../components/fintech-island/assets/images/bg2.png'), networkLogo: require('../../components/fintech-island/assets/images/networkLogo.png'), paypassImage: require('../../components/fintech-island/assets/images/paypass.png'), }, { id: '4', lastFour: '1195', holderName: 'Roy Mustang', expiry: '05/26', cvv: '456', backgroundImage: require('../../components/fintech-island/assets/images/bg1.png'), networkLogo: require('../../components/fintech-island/assets/images/networkLogo.png'), paypassImage: require('../../components/fintech-island/assets/images/paypass.png'), }, ], } const MOCK_SAVINGS = { goalName: 'Emergency Fund', amountSaved: 250, totalSaved: 1250, goalTarget: 2000, currencySymbol: '$', goalImage: require('../../components/fintech-island/assets/images/roy.png'), onViewSavings: () => console.log('View savings pressed'), } export default function DemoScreen() { const islandRef = useRef<FintechIslandRef>(null) const [visible, setVisible] = useState(false) const [activeVariant, setActiveVariant] = useState<'send' | 'cardAdded' | 'savings'>('send') const successTimer = useRef<ReturnType<typeof setTimeout> | null>(null) const handleShowSend = useCallback(() => { if (successTimer.current) clearTimeout(successTimer.current) setActiveVariant('send') setVisible(true) successTimer.current = setTimeout(() => { islandRef.current?.triggerSuccess() }, 2000) }, []) const handleShowCardAdded = useCallback(() => { if (successTimer.current) clearTimeout(successTimer.current) setActiveVariant('cardAdded') setVisible(true) }, []) const handleShowSavings = useCallback(() => { if (successTimer.current) clearTimeout(successTimer.current) setActiveVariant('savings') setVisible(true) }, []) const handleDismiss = useCallback(() => { setVisible(false) if (successTimer.current) clearTimeout(successTimer.current) }, []) const islandProps: FintechIslandProps = activeVariant === 'cardAdded' ? { variant: 'cardAdded', cardAdded: MOCK_CARD_ADDED, visible, onDismiss: handleDismiss, onAddMore: () => { // handle "add another card" action }, } : activeVariant === 'savings' ? { variant: 'savings', savings: MOCK_SAVINGS, visible, onDismiss: handleDismiss, } : { transaction: MOCK_TRANSACTION, visible, onDismiss: handleDismiss, onSendAgain: (tx: RecentTransaction) => { // handle "send again" action }, } return ( <GestureHandlerRootView style={styles.root}> <SafeAreaView style={styles.safe}> <View style={styles.center}> <Text style={styles.title}>FintechIsland</Text> <Text style={styles.subtitle}>Tap a button to preview a variant</Text> <View style={styles.buttonGroup}> <Pressable style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]} onPress={handleShowSend} > <Text style={styles.btnText}>Send</Text> </Pressable> <Pressable style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]} onPress={handleShowCardAdded} > <Text style={styles.btnText}>Card Added</Text> </Pressable> <Pressable style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]} onPress={handleShowSavings} > <Text style={styles.btnText}>Savings</Text> </Pressable> </View> </View> </SafeAreaView> <FintechIsland ref={islandRef} {...islandProps} /> </GestureHandlerRootView> ) } const styles = StyleSheet.create({ root: { flex: 1, }, safe: { flex: 1, backgroundColor: '#000000', }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 24, paddingHorizontal: 32, }, title: { color: '#FFFFFF', fontSize: 28, fontFamily: 'SpaceGroteskMedium', letterSpacing: -0.5, }, subtitle: { color: '#555555', fontSize: 14, marginTop: -16, }, buttonGroup: { width: '100%', gap: 12, marginTop: 16, }, btn: { backgroundColor: '#1A1A1A', paddingVertical: 16, borderRadius: 14, alignItems: 'center', borderWidth: 0.5, borderColor: '#ffffff18', }, btnPressed: { backgroundColor: '#252525', }, btnText: { color: '#FFFFFF', fontSize: 15, fontFamily: 'SpaceGroteskMedium', }, })

API

<FintechIsland> — common props

PropTypeDefaultDescription
variant'send' | 'cardAdded' | 'savings''send'Which variant to render. Determines which data prop is required.
visiblebooleanControls open/close. Animates in on true, morphs out and unmounts on false.
onDismiss() => voidCalled once the dismiss animation finishes — from auto-dismiss, backdrop tap, or ref.dismiss().

send variant

PropTypeDefaultDescription
transactionTransactionDataRequired. Recipient and amount shown on the pill and expanded card.
onSendAgain(transaction: RecentTransaction) => voidCalled when a recent transaction's Send Again button is tapped.

cardAdded variant

PropTypeDefaultDescription
cardAddedCardAddedDataRequired. Cardholder info and the card carousel data.
onAddMore() => voidCalled when Add more is tapped in the expanded card carousel.

savings variant

PropTypeDefaultDescription
savingsSavingsDataRequired. Goal name, amounts, and progress shown on the pill and expanded card.

FintechIslandRef

MethodDescription
triggerSuccess()Resolves a pending send — collapses the expanded card to the pill and shows Sent. No-op for cardAdded and savings.
dismiss()Programmatically dismisses the island from any state.

Types

interface RecentTransaction { id: string; name: string; handle: string; amount: number; currency: string; avatarSource?: ImageSourcePropType; } interface TransactionData { avatarSource?: ImageSourcePropType; recipientName: string; recipientHandle: string; amount: number; currency: string; recentTransactions: RecentTransaction[]; } interface CarouselCard { id: string; lastFour: string; holderName: string; expiry: string; cvv: string; backgroundImage: ImageSourcePropType; paypassImage?: ImageSourcePropType; networkLogo?: ImageSourcePropType; } interface CardAddedData { cardholderName: string; cardholderHandle: string; cardholderAvatar?: ImageSourcePropType; cardBalance: number; currencySymbol?: string; cards: CarouselCard[]; } interface SavingsData { goalName: string; amountSaved: number; totalSaved: number; goalTarget: number; currencySymbol?: string; goalImage?: ImageSourcePropType; onViewSavings?: () => void; } interface FintechIslandRef { triggerSuccess: () => void; dismiss: () => void; }

Project Structure

fintech-island/
├── components/
│ ├── FintechIsland.tsx # Root component — pill, morph, variant switcher
│ ├── SendPanel.tsx # send variant expanded tray
│ ├── CardCarousel.tsx # cardAdded variant — flippable card carousel
│ └── SavingsPanel.tsx # savings variant expanded tray
├── constants/
│ ├── types.ts # FintechIslandProps, TransactionData, CardAddedData, SavingsData
│ ├── island.ts # animation + layout constants
│ └── variants.ts # variant registry (Lottie sources, pill labels)
├── hooks/
│ └── useIslandAnimation.ts # shared animation state for pill ↔ tray morph
└── assets/