Fintech Island
A Dynamic Island–style pill that morphs into a full status tray for sends, card additions, and savings deposits.
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.
Install the needed dependencies
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
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'send' | 'cardAdded' | 'savings' | 'send' | Which variant to render. Determines which data prop is required. |
| visible | boolean | — | Controls open/close. Animates in on true, morphs out and unmounts on false. |
| onDismiss | () => void | — | Called once the dismiss animation finishes — from auto-dismiss, backdrop tap, or ref.dismiss(). |
send variant
| Prop | Type | Default | Description |
|---|---|---|---|
| transaction | TransactionData | — | Required. Recipient and amount shown on the pill and expanded card. |
| onSendAgain | (transaction: RecentTransaction) => void | — | Called when a recent transaction's Send Again button is tapped. |
cardAdded variant
| Prop | Type | Default | Description |
|---|---|---|---|
| cardAdded | CardAddedData | — | Required. Cardholder info and the card carousel data. |
| onAddMore | () => void | — | Called when Add more is tapped in the expanded card carousel. |
savings variant
| Prop | Type | Default | Description |
|---|---|---|---|
| savings | SavingsData | — | Required. Goal name, amounts, and progress shown on the pill and expanded card. |
FintechIslandRef
| Method | Description |
|---|---|
| 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;
}