Real Time Crypto Price Card

Displays a customizable, animated card with real-time cryptocurrency data, including price, percentage change, and a glowing historical price line graph. Fetches data from the CoinGecko API and handles loading/error states.

react-nativeexpotypescriptreanimatedcardgraphchartcryptofinancedata visualizationreact-queryskeleton-loader

Preview

Installation

Step 1: Install Dependencies

For our linegraph

npx expo install react-native-svg

We will be using tanstack query for managing our queries

npm i @tanstack/react-query zustand

Step 2: Create a a file types/coinGecko.types.ts

types that model the JSON response structure from the CoinGecko API.

coinGecko.types.ts
/**
* Represents the currency-specific price data for various metrics.
*/
type CurrencyData = {
  aed: number;
  ars: number;
  aud: number;
  bch: number;
  bdt: number;
  bhd: number;
  bmd: number;
  bnb: number;
  brl: number;
  btc: number;
  cad: number;
  chf: number;
  clp: number;
  cny: number;
  czk: number;
  dkk: number;
  dot: number;
  eos: number;
  eth: number;
  eur: number;
  gbp: number;
  gel: number;
  hkd: number;
  huf: number;
  idr: number;
  ils: number;
  inr: number;
  jpy: number;
  krw: number;
  kwd: number;
  lkr: number;
  ltc: number;
  mmk: number;
  mxn: number;
  myr: number;
  ngn: number;
  nok: number;
  nzd: number;
  php: number;
  pkr: number;
  pln: number;
  rub: number;
  sar: number;
  sek: number;
  sgd: number;
  sol: number;
  thb: number;
  try: number;
  twd: number;
  uah: number;
  usd: number;
  vef: number;
  vnd: number;
  xag: number;
  xau: number;
  xdr: number;
  xlm: number;
  xrp: number;
  yfi: number;
  zar: number;
  bits: number;
  link: number;
  sats: number;
}

export type Currency = keyof CurrencyData;

export type TimeFrame = "24h" | "7d" | "30d" | "1y";

/**
 * Represents the currency-specific date data for ATH/ATL.
 */
type CurrencyDateData = {
  [currency: string]: string; // Allows any currency code as a key
}

/**
 * Represents links to code repositories.
 */
type ReposUrl = {
  github: string[];
  bitbucket: string[];
}

/**
 * Represents all external links related to the coin.
 */
type Links = {
  homepage: string[];
  whitepaper: string;
  blockchain_site: string[];
  official_forum_url: string[];
  chat_url: string[];
  announcement_url: string[];
  snapshot_url: string | null;
  twitter_screen_name: string;
  facebook_username: string;
  bitcointalk_thread_identifier: number | null;
  telegram_channel_identifier: string;
  subreddit_url: string;
  repos_url: ReposUrl;
}

/**
 * Represents different sizes of the coin's image.
 */
type Image = {
  thumb: string;
  small: string;
  large: string;
}

/**
 * Represents the detailed market data for the coin.
 */
type MarketData = {
  current_price: CurrencyData;
  total_value_locked: null; // Or a specific type if it can have a value
  mcap_to_tvl_ratio: null;
  fdv_to_tvl_ratio: null;

  // All-time high price in various currencies
  ath: CurrencyData;
  // Percentage change from all-time high in various currencies
  ath_change_percentage: CurrencyData;
  // Date of all-time high in various currencies
  ath_date: CurrencyDateData;

  // All-time low price in various currencies
  atl: CurrencyData;
  // Percentage change from all-time low in various currencies
  atl_change_percentage: CurrencyData;
  // Date of all-time low in various currencies
  atl_date: CurrencyDateData;

  // Market capitalization in various currencies
  market_cap: CurrencyData;
  market_cap_rank: number;
  
  // Fully diluted valuation in various currencies
  fully_diluted_valuation: CurrencyData;
  market_cap_fdv_ratio: number;
  
  // Total volume in various currencies
  total_volume: CurrencyData;

  // High and low price in the last 24h in various currencies
  high_24h: CurrencyData;
  low_24h: CurrencyData;
  
  price_change_24h: number;
  price_change_percentage_24h: number;
  price_change_percentage_7d: number;
  price_change_percentage_14d: number;
  price_change_percentage_30d: number;
  price_change_percentage_60d: number;
  price_change_percentage_200d: number;
  price_change_percentage_1y: number;

  market_cap_change_24h: number;
  market_cap_change_percentage_24h: number;

  // Price changes over various time frames in different currencies
  price_change_24h_in_currency: CurrencyData;
  price_change_percentage_1h_in_currency: CurrencyData;
  price_change_percentage_24h_in_currency: CurrencyData;
  price_change_percentage_7d_in_currency: CurrencyData;
  price_change_percentage_14d_in_currency: CurrencyData;
  price_change_percentage_30d_in_currency: CurrencyData;
  price_change_percentage_60d_in_currency: CurrencyData;
  price_change_percentage_200d_in_currency: CurrencyData;
  price_change_percentage_1y_in_currency: CurrencyData;
  
  market_cap_change_24h_in_currency: CurrencyData;
  market_cap_change_percentage_24h_in_currency: CurrencyData;
  
  total_supply: number;
  max_supply: number | null;
  circulating_supply: number;
  last_updated: string;
}

/**
 * The main type for the full response from the /coins/{id} endpoint.
 */
export type CoinDetail = {
  id: string;
  symbol: string;
  name: string;
  web_slug: string;
  asset_platform_id: null; // Or a specific type
  platforms: { [key: string]: string };
  detail_platforms: { [key: string]: { decimal_place: number | null; contract_address: string } };
  block_time_in_minutes: number;
  hashing_algorithm: string;
  categories: string[];
  preview_listing: boolean;
  public_notice: null; // Or string
  additional_notices: any[];
  description: { [language_code: string]: string };
  links: Links;
  image: Image;
  country_origin: string;
  genesis_date: string; // ISO 8601 date string
  sentiment_votes_up_percentage: number;
  sentiment_votes_down_percentage: number;
  watchlist_portfolio_users: number;
  market_cap_rank: number;
  market_data: MarketData;
  status_updates: any[]; // Or a specific type if you know the structure
  last_updated: string; // ISO 8601 date string
}

Step 3: Add a utility throttledFetch.ts (optional)

An utility that wraps the standard fetch call to control the frequency of API requests, preventing rate-limit errors from api

throttledFetch.ts
// This queue will hold all pending requests.
// Each item will be a promise's resolve/reject functions and the fetch arguments.
const requestQueue: {
   url: string;
   options: RequestInit;
   resolve: (value: any) => void;
   reject: (reason?: any) => void;
}[] = [];

let isProcessing = false;
const RATE_LIMIT_INTERVAL = 2100; // 30 requests/min is 1 every 2s. We add a 100ms buffer.

/**
* Processes the request queue one by one, with a delay between each request.
*/
function processQueue() {
   if (isProcessing) return; // Don't start a new interval if one is already running
   isProcessing = true;

   const intervalId = setInterval(async () => {
   if (requestQueue.length === 0) {
       // If the queue is empty, stop the interval and mark as not processing.
       clearInterval(intervalId);
       isProcessing = false;
       return;
   }

   // Get the next request from the front of the queue.
   const nextRequest = requestQueue.shift();
   if (nextRequest) {
       try {
       const response = await fetch(nextRequest.url, nextRequest.options);
       if (!response.ok) {
           const errorData = await response.json();
           // Reject the specific promise for this request with the error.
           nextRequest.reject(new Error(errorData.error || 'API request failed'));
       } else {
           const data = await response.json();
           // Resolve the specific promise for this request with the data.
           nextRequest.resolve(data);
       }
       } catch (error) {
       nextRequest.reject(error);
       }
   }
   }, RATE_LIMIT_INTERVAL);
}

/**
* A throttled version of fetch. Instead of fetching immediately, it adds
* the request to a queue that is processed at a rate-limit-friendly speed.
* @param url The URL to fetch.
* @param options The fetch options.
* @returns A promise that resolves/rejects when the request is eventually processed.
*/
export const throttledFetch = (url: string, options: RequestInit): Promise<any> => {
return new Promise((resolve, reject) => {
 // Add the new request to the end of the queue.
 requestQueue.push({ url, options, resolve, reject });
 // Start processing the queue if it's not already running.
 processQueue();
});
};

Step 4: Create a file hooks/useCryptoData.ts

Provides a collection of custom TanStack Query hooks to fetch, cache, and manage cryptocurrency market data, price history, and details from the CoinGecko API.

useCryptoData.ts
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { throttledFetch } from '@/lib/apiClient';
import { CoinDetail } from '@/types/coinGecko.types';
import { Currency, TimeFrame } from "@/types/coinGecko.types";

export type Roi = {
times: number;
currency: string;
percentage: number;
}

// types
export type Coin = {
id: string;
symbol: string;
name: string;
image: string;
current_price: number;
market_cap: number;
market_cap_rank: number;
fully_diluted_valuation: number | null;
total_volume: number;
high_24h: number;
low_24h: number;
price_change_24h: number;
price_change_percentage_24h: number;
market_cap_change_24h: number;
market_cap_change_percentage_24h: number;
circulating_supply: number;
total_supply: number | null;
max_supply: number | null;
ath: number;
ath_change_percentage: number;
ath_date: string;
atl: number;
atl_change_percentage: number;
atl_date: string;
roi: Roi | null;
last_updated: string;
price_change_percentage_24h_in_currency: number;
price_change_percentage_7d_in_currency: number;
price_change_percentage_30d_in_currency: number;
price_change_percentage_1y_in_currency: number;
}

// Type for the full API response object
export type CoinApiResponse = {
data: Coin[];
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
}

const COINGECKO_API_KEY = 'your-api-key';

/**
* Fetches a paginated list of all coins from the CoinGecko API.
*
* @param page - The page number to fetch (default: 1)
* @param pageSize - The number of coins per page (default: 10)
* @param timeFrame - The time frame for price change percentage (default: '24h'). 
*                    Accepts '24h', '7d', '30d' or 1y.
* @returns A Promise that resolves to an array of Coin objects.
*/
const fetchAllCoins = async (page: number = 1, pageSize: number = 10, timeFrame: TimeFrame = '24h', currency: Currency): Promise<Coin[]> => {
const url = `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${currency}&price_change_percentage=${timeFrame}&order=market_cap_desc&per_page=${pageSize}&page=${page}`;

const options = {
method: 'GET',
headers: {
  'x-cg-demo-api-key': COINGECKO_API_KEY,
},
};

try {
const response = await fetch(url, options);

if (!response.ok) {
  // If the response is not OK, try to read it as text first.
  const errorText = await response.text();
  console.error("API Error Response Text:", errorText);

  if (errorText.toLowerCase().includes('throttled')) {
    throw new Error('Rate limit exceeded. Please wait a moment.');
  }
  
  // If it's something else, try to parse it as JSON, or just throw the text.
  try {
      const errorJson = JSON.parse(errorText);
      throw new Error(errorJson.error || 'An unknown API error occurred');
  } catch (e) {
      throw new Error(`API returned status ${response.status}: ${errorText}`);
  }
}

return response.json();

} catch (error) {
// This will catch network errors (e.g., no internet)
console.error("Fetch operation failed:", error);
throw error; // Re-throw the error for TanStack Query to handle
}
};


/**
* Fetches the OHLC price history for a single coin from the official CoinGecko API.
* @param coinId - The string ID of the coin (e.g., "bitcoin")
* @param days - The number of days for the history (e.g., 7)
*/
const fetchCoinOhlc = async (coinId: string, days: number = 7, currency: Currency): Promise<number[]> => {
// Construct the URL using the coinId string directly
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc?vs_currency=${currency}&days=${days}`;

const options = {
    method: 'GET',
    headers: {
    'x-cg-demo-api-key': COINGECKO_API_KEY,
    },
};

const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.json();
console.error("CoinGecko OHLC API Error:", errorData);
throw new Error(errorData.error || 'Failed to fetch OHLC data');
}

// The new data structure is an array of arrays: [timestamp, open, high, low, close]
const data: [number, number, number, number, number][] = await throttledFetch(url, options); //throttledFetch is you using free version of the api, added due to rate limits 

// We need to extract the 5th element (index 4), which is the closing price.
return data.map(pricePoint => pricePoint[4]);
};



const fetchSingleCoin = async (coinIdentifier: string): Promise<CoinDetail> => {
const url = `https://api.coingecko.com/api/v3/coins/${coinIdentifier}`;
const options = {
    method: 'GET',
    headers: { 'x-cg-demo-api-key': COINGECKO_API_KEY, },
};

const response = await fetch(url, options);
if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.error || 'Failed to fetch coin details');
}

// The function now returns the full, strongly-typed object.
return response.json();
};

// Hooks 

export function useInfiniteCoinsQuery(timeFrame: TimeFrame, currency: Currency) {
return useInfiniteQuery({
    queryKey: ['allCoins'],
    queryFn: ({ pageParam = 1 }) => fetchAllCoins(pageParam, 10, timeFrame, currency),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
    // If the last page had items, request the next page.
    // If the last page was empty, return undefined to stop fetching.
    return lastPage.length > 0 ? allPages.length + 1 : undefined;
    },
});
}

// Hook (used inside each card)
export function useCoinPriceHistoryQuery(coinId: string, timeFrame: TimeFrame, currency: Currency) {
let days: number;
switch (timeFrame) {
    case '24h': days = 1; break;
    case '7d': days = 7; break;
    case '30d': days = 30; break;
    case '1y': days = 365; break;
    default: days = 7;
}
return useQuery({
    queryKey: ['coinOhlc', coinId],
    queryFn: () => fetchCoinOhlc(coinId, days, currency),
    staleTime: 1000 * 60 * 5, // you can remove and add like refresh control on demand
    refetchInterval: 1000 * 60 * 5, // you can remove and add like refresh control on demand
    enabled: !!coinId,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), //this is to slow down if you are using free version of the api
    retry: 2,
});
}

// Hook to fetch a single coin by name/symbol
export function useSingleCoinQuery(coinIdentifier: string) {
return useQuery<CoinDetail, Error>({
    queryKey: ['singleCoin', coinIdentifier.toLowerCase()],
    queryFn: () => fetchSingleCoin(coinIdentifier),
    staleTime: 1000 * 60 * 2, // you can remove and add like refresh control on demand
    refetchInterval: 1000 * 60 * 1, // you can remove and add like refresh control on demand
    enabled: !!coinIdentifier, // Only fetch if coinIdentifier is provided
    retry: 2,
});
}

Step 5: Add Skeleton from Components

Step 6: Create a Card loader component

Renders a skeleton placeholder that mimics the AnimatedCryptoCard layout, while data is being fetched.

CardLoader.tsx
import React from "react";
import { View, StyleSheet } from "react-native";
import Skeleton from "./Skeleton";

export const CardLoader = ({
cardBgColor,
reduceMotion,
}: {
cardBgColor?: string;
reduceMotion?: "always" | "never" | "system";
}) => {
return (
	<View style={[styles.loadingCard, { backgroundColor: cardBgColor }]}>
		<View style={styles.header}>
			<View style={styles.titleContainer}>
				{/* Skeleton for the Icon */}
				<Skeleton
					isLoading={true}
					baseColor={"#4B4B4D"}
					shimmerColor={"#2C2F2F"}
					style={styles.skeletonIcon}
					reduceMotion={reduceMotion}
				/>
			</View>
			{/* Skeleton for the Percentage Badge */}
			<Skeleton
				isLoading={true}
				baseColor={"#4B4B4D"}
				shimmerColor={"#2C2F2F"}
				style={styles.skeletonBadge}
				reduceMotion={reduceMotion}
			/>
		</View>

		<View style={styles.footer}>
			{/* Skeleton for the Price */}
			<Skeleton
				isLoading={true}
				baseColor={"#4B4B4D"}
				shimmerColor={"#2C2F2F"}
				style={styles.skeletonPrice}
				reduceMotion={reduceMotion}
			/>
		</View>
	</View>
);
};

const styles = StyleSheet.create({
loadingCard: {
	borderRadius: 24,
	padding: 20,
	marginVertical: 8,
	minHeight: 150,
    width: '100%',
},
header: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-start",
	marginBottom: 22,
},
titleContainer: {
	flexDirection: "row",
	alignItems: "center",
},
footer: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-end",
},
skeletonIcon: {
	width: 40,
	height: 40,
	borderRadius: 20,
	marginRight: 12,
},
skeletonBadge: {
	height: 28,
	width: 70,
	borderRadius: 12,
},
skeletonPrice: {
	height: 36,
	width: 140,
	borderRadius: 4,
},
});

Step 7: Create a line graph component for the crypto card AnimatedLineGraph.tsx

uses React-native-svg to plot path, uses reanimated to animated the last plot with glowing

AnimatedLineGraph.tsx
import React, { useMemo } from "react";
import { View } from "react-native";
import Animated, { SharedValue, useAnimatedProps } from "react-native-reanimated";
import Svg, { Path, Defs, LinearGradient, Stop, Circle } from "react-native-svg";

type LineGraphProps = {
data: number[];
width: number;
height: number;
color?: string;
glowOpacity: SharedValue<number>;
};
const VERTICAL_PADDING = 4;
const HORIZONTAL_PADDING = 4;

const AnimatedCircle = Animated.createAnimatedComponent(Circle);

/**
* Generates an SVG path string for a line graph based on the provided data points.
*
* - Each data point is mapped to an (x, y) coordinate:
*    - x is spaced evenly across the width (minus horizontal padding).
*    - y is scaled so the min value is at the bottom and the max at the top, with vertical padding.
* - The path starts with 'M' (move to) for the first point, then 'L' (line to) for each subsequent point.
*
* @param data - Array of y-values (numbers) to plot.
* @param width - Width of the SVG area.
* @param height - Height of the SVG area.
* @returns SVG path string for the line graph.
*/
const createPath = (data: number[], width: number, height: number): string => {
if (!data || data.length < 2) {
	// Not enough data to draw a line, return a flat line at the bottom
	return `M 0,${height} L ${width},${height}`;
}

const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;

return data.reduce((acc, point, i) => {
	const x = (i / (data.length - 1)) * (width - 2 * HORIZONTAL_PADDING);
	const y =
		VERTICAL_PADDING +
		(height - 2 * VERTICAL_PADDING - ((point - min) / range) * (height - 2 * VERTICAL_PADDING));
	return i === 0 ? `M ${x},${y}` : `${acc} L ${x},${y}`;
}, "");
};

const getLastPointCoordinates = (data: number[], width: number, height: number) => {
if (!data || data.length === 0) {
	return { x: width, y: height };
}
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;

const lastPoint = data[data.length - 1];
const x = width - 2 * HORIZONTAL_PADDING;
const y =
	VERTICAL_PADDING + (height - 2 * VERTICAL_PADDING) - ((lastPoint - min) / range) * (height - 2 * VERTICAL_PADDING);

return { x, y };
};

export const AnimatedLineGraph = ({
data,
width,
height,
color = "#C9FE71",
glowOpacity,
}: LineGraphProps) => {
const linePath = useMemo(() => createPath(data, width, height), [data, width, height]);

const areaPath = useMemo(
	() => `${linePath} L ${width - 2 * HORIZONTAL_PADDING},${height} L ${HORIZONTAL_PADDING},${height} Z`,
	[linePath, width, height]
);

const endPoint = useMemo(() => getLastPointCoordinates(data, width, height), [data, width, height]);

const animatedGlowProps = useAnimatedProps(() => {
	return {
		opacity: glowOpacity.value,
	};
});

return (
	<View style={{ width, height }}>
		<Svg width={width} height={height}>
			<Defs>
				<LinearGradient id="gradient" x1="50%" y1="20%" x2="50%" y2="100%">
					<Stop offset="0%" stopColor={color} stopOpacity={0.3} />
					<Stop offset="100%" stopColor={color} stopOpacity={0} />
				</LinearGradient>
			</Defs>
			<Path d={areaPath} fill="url(#gradient)" />
			<Path
				d={linePath}
				fill="transparent"
				stroke={color}
				strokeWidth={2.5}
				strokeLinejoin="round"
				strokeLinecap="round"
			/>
			<AnimatedCircle
				cx={endPoint.x}
				cy={endPoint.y}
				animatedProps={animatedGlowProps}
				r={5}
				fill={color}
			/>
			<Circle cx={endPoint.x} cy={endPoint.y} r={2} fill={color} />
		</Svg>
	</View>
);
};

Step 8: Finally Create file AnimatedCryptoCard.tsx

Import all the files created above please check imports it may not be same as yours thank you

AnimatedCryptoCard.tsx
import React, { useEffect, useMemo } from "react";
import { View, Text, StyleSheet, Image } from "react-native";
import { useSingleCoinQuery, useCoinPriceHistoryQuery } from "@/hooks/useCryptoData";
import Skeleton from "./Skeleton";
import { ReduceMotion, useSharedValue, withRepeat, withSequence, withTiming, Easing } from "react-native-reanimated";
import { AnimatedLineGraph } from "./AnimatedLineGraph";
import { CardLoader } from "./CardLoader";
import { Currency, TimeFrame } from "@/types/coinGecko.types";

type AnimatedCryptoCardProps = {
timeFrame?: TimeFrame;
currency?: Currency
coinIdentifier: string; //must be like bitcoin, ethereum etc you can go to coin in coin gecko and copy the api id
positiveColor?: string;
negativeColor?: string;
cardBgColor?: string;
symbolColor?: string;
nameColor?: string;
priceColor?: string;
timeFrameColor?: string;
reduceMotion?: "always" | "never" | "system";
};

export const AnimatedCryptoCard = ({
coinIdentifier,
currency = 'usd',
timeFrame = "1y",
positiveColor = "#C9FE71",
negativeColor = "#FF6B6B",
cardBgColor = "#1E1E1E",
symbolColor = "#FFFFFF",
nameColor = "#8A8A8A",
priceColor = "#FFFFFF",
timeFrameColor = "#2C2F2F",
reduceMotion,
}: AnimatedCryptoCardProps) => {
const glowOpacity = useSharedValue<number>(0.2);
const motion =
	reduceMotion === "never"
		? ReduceMotion.Never
		: reduceMotion === "always"
			? ReduceMotion.Always
			: ReduceMotion.System;
//query
const {
	data: coin,
	isLoading: coinLoading,
	isError: coinError,
	error: coinErrorMessage,
} = useSingleCoinQuery(coinIdentifier);
// Then fetch the price history using the productId from the coin data
const { data: history, isLoading: historyLoading, isError: historyError } = useCoinPriceHistoryQuery(coin?.id || "", timeFrame, currency);

//reanimated
useEffect(() => {
	glowOpacity.value = withRepeat(
		withSequence(
			withTiming(1, { duration: 1500, easing: Easing.inOut(Easing.ease), reduceMotion: motion }),
			withTiming(0.2, { duration: 1500, easing: Easing.inOut(Easing.ease), reduceMotion: motion })
		),
		-1, // Repeat indefinitely
		true, // Reverse the animation smoothly
		() => {},
		motion
	);
}, []);

//data on the card
const displayData = useMemo(() => {
    if (!coin) {
        // Return default values if coin data is not yet loaded
        return {
            price: 0,
            percentageChange: 0,
            isPositive: true,
        };
    }

    // Select the correct percentage change based on the timeFrame prop
    let percentageChange: number;
    switch (timeFrame) {
        case '24h':
            percentageChange = coin.market_data.price_change_percentage_24h_in_currency[currency];
            break;
        case '7d':
            percentageChange = coin.market_data.price_change_percentage_7d_in_currency[currency];
            break;
        case '30d':
            percentageChange = coin.market_data.price_change_percentage_30d_in_currency[currency];
            break;
        case '1y':
            percentageChange = coin.market_data.price_change_percentage_1y_in_currency[currency];
            break;
        default:
            percentageChange = coin.market_data.price_change_percentage_24h;
    }

    return {
        price: coin.market_data.current_price[currency],
        percentageChange: percentageChange || 0, // Fallback to 0 if the value is null/undefined
        isPositive: (percentageChange || 0) >= 0,
    };

}, [coin, timeFrame]);

// Format price
const formattedPrice = useMemo(() => {
	const price = displayData.price;

	const options: Intl.NumberFormatOptions = {
		style: "currency",
		currency: currency.toUpperCase(),
	};

	if (price >= 100000) {
		options.minimumFractionDigits = 0;
		options.maximumFractionDigits = 0;
	} else if (price >= 1) {
		options.minimumFractionDigits = 2;
		options.maximumFractionDigits = 2;
	} else if (price > 0) {
		// 1. Calculate the number of leading zeros after the decimal.
		// Math.log10(0.000013689) is approx -4.86. Flipping and flooring gives 4.
		const numberOfLeadingZeros = Math.floor(-Math.log10(price));
		// 2. Set the maximum fraction digits to be the number of zeros plus the 2 significant digits you want.
		options.maximumFractionDigits = numberOfLeadingZeros + 2;
		// We still set a minimum to avoid showing just "$0.00" for slightly larger fractions.
		options.minimumFractionDigits = 2;

	} else {
		// For a price of exactly 0, use standard formatting.
		options.minimumFractionDigits = 2;
		options.maximumFractionDigits = 2;
	}

	return new Intl.NumberFormat("en-US", options).format(price);

}, [displayData.price, currency]);


// Loading state - show loading until we have coin data
if (coinLoading) {
	return <CardLoader cardBgColor={cardBgColor} reduceMotion={motion} />;
}

// Error state - show error if coin fetch failed
if (coinError || !coin) {
	return (
		<View style={[styles.card, styles.errorCard, { backgroundColor: cardBgColor }]}>
			<Text style={[styles.errorText, { color: negativeColor }]}>Failed to load {coinIdentifier}</Text>
			<Text style={[styles.errorSubtext, { color: nameColor }]}>{coinErrorMessage?.message || "Coin not found"}</Text>
		</View>
	);
}

// const isPositive = displayData.isPositive >= 0;
const percentageColor = displayData.isPositive ? positiveColor : negativeColor;
const percentageBackgroundColor = displayData.isPositive ? `${positiveColor}26` : `${negativeColor}26`;

return (
	<View style={[styles.card, { backgroundColor: cardBgColor }]}>
		<View style={styles.header}>
			<View style={styles.titleContainer}>
				<Image source={{ uri: coin.image.large }} style={styles.icon} />
				<View>
					<Text style={[styles.symbol, { color: symbolColor }]} numberOfLines={1}>{coin.symbol.toUpperCase()}</Text>
					<Text style={[styles.name, { color: nameColor }]} numberOfLines={1}>{coin.name}</Text>
				</View>
			</View>
			<View style={{ flexDirection: "row", gap: 8 }}>
				<View style={[styles.changeContainer, { backgroundColor: timeFrameColor }]}>
					<Text style={[styles.timeFrameText, { color: nameColor }]}>{timeFrame}</Text>
				</View>
				<View style={[styles.changeContainer, { backgroundColor: percentageBackgroundColor }]}>
					<Text style={[styles.changeText, { color: percentageColor }]}>
						{displayData.isPositive ? "+" : ""}
						{displayData.percentageChange.toFixed(2)} %
					</Text>
				</View>
			</View>
		</View>

		<View style={styles.footer}>
			<Text style={[styles.price, { color: priceColor }]}>{formattedPrice}</Text>
			<View style={styles.graphContainer}>
				{historyLoading ? (
					<Skeleton
						isLoading={true}
						baseColor={"#4B4B4D"}
						shimmerColor={"#2C2F2F"}
						style={styles.skeletonGraph}
						reduceMotion={reduceMotion}
					/>
				) : // <ActivityIndicator color={loadingColor} />
				historyError || !history ? (
					<Text style={[styles.chartErrorText, { color: negativeColor }]}>Chart unavailable</Text>
				) : (
					<AnimatedLineGraph
						data={history}
						color={percentageColor}
						width={150}
						height={50}
						glowOpacity={glowOpacity}
					/>
				)}
			</View>
		</View>
	</View>
);
};

const styles = StyleSheet.create({
card: {
	borderRadius: 24,
	padding: 20,
	marginVertical: 8,
	width: '100%',
},
errorCard: {
	justifyContent: "center",
	alignItems: "center",
	minHeight: 150,
},
loadingText: {
	marginTop: 12,
	fontSize: 16,
},
errorText: {
	fontSize: 16,
	fontWeight: "600",
	textAlign: "center",
},
errorSubtext: {
	fontSize: 14,
	marginTop: 4,
	textAlign: "center",
},
chartErrorText: {
	fontSize: 12,
},
header: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-start",
	marginBottom: 22,
},
titleContainer: {
	flex: 1,
	flexDirection: "row",
	alignItems: "center",
},
icon: {
	width: 40,
	height: 40,
	borderRadius: 20,
	marginRight: 12,
},
symbol: {
	fontSize: 20,
	fontWeight: "600",
},
name: {
	fontSize: 15,
},
changeContainer: {
	borderRadius: 12,
	paddingHorizontal: 10,
	paddingVertical: 6,
},
changeText: {
	fontWeight: "600",
	fontSize: 14,
},
footer: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-end",
},
price: {
	fontSize: 26,
	fontWeight: "700",
},
graphContainer: {
	width: 150,
	height: 50,
	justifyContent: "center",
	alignItems: "center",
},
timeFrameText: {
	fontWeight: "600",
	fontSize: 14,
},
skeletonGraph: {
	height: 50,
	width: 150,
	borderRadius: 8,
},
});

Usage

You can customize currency timeframe, animated yes or no, colors

SingleCoinPage.tsx
import React from 'react';
import { SafeAreaView, StyleSheet, ScrollView , Text} from 'react-native';
import { AnimatedCryptoCard } from '@/components/ui/AnimatedCryptoCard';
import { useAppColors } from '@/hooks/useAppColors';

export default function SingleCoinPage() {
const colors = useAppColors();
return (
  <SafeAreaView >
    <ScrollView contentContainerStyle={styles.screenContainer}>
    <Text style={[styles.title, { color: colors.Neutral900 }]}>Crpto Price Card Examples</Text>
      
      <Text style={[styles.sliderLabel, { color: colors.Neutral700 }]}>1. BTC, USD, 1y, animated true</Text>
      <AnimatedCryptoCard coinIdentifier="bitcoin" currency='usd' timeFrame='1y' reduceMotion='never'/>
      <Text style={[styles.sliderLabel, { color: colors.Neutral700 }]}>2. SOL, GBP, 24h, animated false</Text>
      <AnimatedCryptoCard coinIdentifier="solana" currency='gbp' timeFrame='24h' />
      <Text style={[styles.sliderLabel, { color: colors.Neutral700 }]}>3. ETH, AED, 30d, animated true</Text>
      <AnimatedCryptoCard 
        coinIdentifier="ethereum" 
        currency='aed'
        timeFrame='30d'
        positiveColor="#9945FF"
        negativeColor="#FF4444"
        cardBgColor="#2A2A2A"
        reduceMotion='never'
      />
      <Text style={[styles.sliderLabel, { color: colors.Neutral700 }]}>3. TRUMP, JPY, 7d, animated true</Text>
      <AnimatedCryptoCard 
        coinIdentifier="official-trump" 
        currency='jpy'
        timeFrame='7d'
        positiveColor="#9945FF"
        negativeColor="#FF4444"
        cardBgColor="#2A2A2A"
        reduceMotion='never'
      />
     
    </ScrollView>
  </SafeAreaView>
);
}

const styles = StyleSheet.create({
screenContainer: {
  	flexGrow: 1,
  	alignItems: "center",
  	paddingVertical: 20,
  	paddingHorizontal: 10,
  },
  title: {
  	fontSize: 24,
  	fontWeight: "bold",
  	marginBottom: 30,
  	textAlign: "center",
  }, 
  sliderSection: {
  	marginBottom: 40,
  	width: "100%",
  	alignItems: "center", // Center the slider component itself
  },
  sliderLabel: {
  	fontSize: 16,
  	fontWeight: "600",
  	marginBottom: 15,
  },
});

Props

PropTypeDefaultRequiredDescription
coinIdentifierstringYesThe unique CoinGecko API ID for the cryptocurrency (e.g., 'bitcoin', 'ethereum') go to any coin in coin gecko and copy the api id.
timeFrame"24h" | "7d" | "30d" | "1y"'1y'NoSpecifies the time period for the price history graph and percentage change.
currencyCurrency'usd'NoThe currency for displaying prices and market data, based on CoinGecko's supported currencies.
positiveColorstring'#C9FE71'NoThe color used for the line graph and percentage text when the change is positive.
negativeColorstring'#FF6B6B'NoThe color used for the line graph and percentage text when the change is negative.
cardBgColorstring'#1E1E1E'NoThe background color of the card component.
symbolColorstring'#FFFFFF'NoThe color of the coin's symbol text (e.g., 'BTC').
nameColorstring'#8A8A8A'NoThe color of the coin's full name text (e.g., 'Bitcoin').
priceColorstring'#FFFFFF'NoThe color of the main price display text.
timeFrameColorstring'#2C2F2F'NoThe background color of the time frame indicator badge.
reduceMotion"always" | "never" | "system"systemNoControls animation behavior to respect the user's accessibility settings.
Performance Warning:

Avoid using the AnimatedCryptoCard component directly inside a FlashList each card will have reanimated shared value to run the animation that will cause huge performace degrading. Refer to this page component for using it inside lists.