
Шторки, всплывающие над затемнением — формы, акции, оплата и вход.
npx astra-rn add apple-v1pnpm dlx astra-rn add apple-v1yarn dlx astra-rn add apple-v1bunx --bun astra-rn add apple-v1npm install @gorhom/bottom-sheet expo-router react-native-safe-area-context react-native-svgpnpm add @gorhom/bottom-sheet expo-router react-native-safe-area-context react-native-svgyarn add @gorhom/bottom-sheet expo-router react-native-safe-area-context react-native-svgbun add @gorhom/bottom-sheet expo-router react-native-safe-area-context react-native-svgimport React, { memo, useCallback, useMemo, useRef, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useFocusEffect } from "expo-router";
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetView,
type BottomSheetBackdropProps,
} from "@gorhom/bottom-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { G, Path } from "react-native-svg";
import type { IApplePayBottomSheet, IGlyph } from "./types";
import {
APPLE_GROUP_TRANSFORM,
APPLE_PATH,
APPLE_VIEW_BOX,
BACKDROP_OPACITY,
COLORS,
CONTENT_BOTTOM_PADDING,
CONTENT_HORIZONTAL_PADDING,
DEFAULT_ACTION_LABEL,
DEFAULT_AMOUNT,
DEFAULT_BRAND_LABEL,
DEFAULT_CARD_CAPTION,
DEFAULT_CARD_NAME,
DEFAULT_DESCRIPTION,
DEFAULT_FOOTNOTE,
SHEET_CORNER_RADIUS,
VISA_PATH,
VISA_VIEW_BOX,
} from "./const";
const AppleGlyph: React.FC<IGlyph> = ({
size = 30,
color = COLORS.ink,
}: IGlyph) => (
<Svg width={size} height={size} viewBox={APPLE_VIEW_BOX}>
<G transform={APPLE_GROUP_TRANSFORM}>
<Path fill={color} d={APPLE_PATH} />
</G>
</Svg>
);
const VisaGlyph: React.FC<IGlyph> = ({
size = 30,
color = COLORS.badgeMark,
}: IGlyph) => (
<Svg width={size} height={size} viewBox={VISA_VIEW_BOX}>
<Path fill={color} d={VISA_PATH} />
</Svg>
);
const ApplePayBottomSheet: React.FC<IApplePayBottomSheet> = ({
brandLabel = DEFAULT_BRAND_LABEL,
description = DEFAULT_DESCRIPTION,
cardName = DEFAULT_CARD_NAME,
cardCaption = DEFAULT_CARD_CAPTION,
amount = DEFAULT_AMOUNT,
footnote = DEFAULT_FOOTNOTE,
actionLabel = DEFAULT_ACTION_LABEL,
openOnFocus = true,
onCardPress,
onActionPress,
onClose,
}: IApplePayBottomSheet) => {
const sheetRef = useRef<BottomSheet>(null);
const insets = useSafeAreaInsets();
const [isMounted, setIsMounted] = useState<boolean>(openOnFocus);
const [isCardPressed, setIsCardPressed] = useState<boolean>(false);
const [isActionPressed, setIsActionPressed] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
if (openOnFocus) setIsMounted(true);
return () => {
sheetRef.current?.close();
setIsMounted(false);
};
}, [openOnFocus]),
);
const handleChange = useCallback(
(index: number): void => {
if (index !== -1) return;
setIsMounted(false);
onClose?.();
},
[onClose],
);
const handleCardPressIn = useCallback((): void => setIsCardPressed(true), []);
const handleCardPressOut = useCallback(
(): void => setIsCardPressed(false),
[],
);
const handleActionPressIn = useCallback(
(): void => setIsActionPressed(true),
[],
);
const handleActionPressOut = useCallback(
(): void => setIsActionPressed(false),
[],
);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
opacity={BACKDROP_OPACITY}
pressBehavior="close"
/>
),
[],
);
const contentStyle = useMemo(
() => [
styles.content,
{ paddingBottom: CONTENT_BOTTOM_PADDING + insets.bottom },
],
[insets.bottom],
);
if (!isMounted) return null;
return (
<BottomSheet
ref={sheetRef}
index={0}
enablePanDownToClose
enableDynamicSizing
onChange={handleChange}
backdropComponent={renderBackdrop}
handleComponent={null}
backgroundStyle={styles.background}
style={styles.sheet}
>
<BottomSheetView style={contentStyle}>
<View style={styles.brand}>
<AppleGlyph />
<Text style={styles.brandLabel}>{brandLabel}</Text>
</View>
<Text style={styles.description}>{description}</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={`${cardName} ${amount}`}
onPress={onCardPress}
onPressIn={handleCardPressIn}
onPressOut={handleCardPressOut}
style={[styles.card, isCardPressed ? styles.cardPressed : null]}
>
<View style={styles.badge}>
<VisaGlyph />
</View>
<View style={styles.cardText}>
<Text style={styles.cardName} numberOfLines={1}>
{cardName}
</Text>
{cardCaption ? (
<Text style={styles.cardCaption} numberOfLines={1}>
{cardCaption}
</Text>
) : null}
</View>
<Text style={styles.amount} numberOfLines={1}>
{amount}
</Text>
</Pressable>
<View style={styles.divider} />
<Text style={styles.footnote}>{footnote}</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={actionLabel}
onPress={onActionPress}
onPressIn={handleActionPressIn}
onPressOut={handleActionPressOut}
style={[styles.action, isActionPressed ? styles.actionPressed : null]}
>
<Text style={styles.actionLabel} numberOfLines={1}>
{actionLabel}
</Text>
</Pressable>
</BottomSheetView>
</BottomSheet>
);
};
const styles = StyleSheet.create({
sheet: {
shadowColor: "#000000",
shadowOpacity: 0.18,
shadowRadius: 24,
shadowOffset: { width: 0, height: -6 },
elevation: 16,
},
background: {
backgroundColor: COLORS.sheet,
borderTopLeftRadius: SHEET_CORNER_RADIUS,
borderTopRightRadius: SHEET_CORNER_RADIUS,
},
content: {
alignSelf: "stretch",
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
paddingTop: 26,
},
brand: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 2,
},
brandLabel: {
fontSize: 34,
lineHeight: 38,
fontWeight: "600",
letterSpacing: -1,
color: COLORS.ink,
},
description: {
alignSelf: "center",
marginTop: 16,
fontSize: 16,
maxWidth: 280,
lineHeight: 22,
color: COLORS.body,
textAlign: "center",
},
card: {
flexDirection: "row",
alignItems: "center",
gap: 14,
marginTop: 26,
borderRadius: 12,
paddingVertical: 8,
},
cardPressed: {
backgroundColor: COLORS.rowPressed,
},
badge: {
width: 46,
height: 30,
borderRadius: 6,
backgroundColor: COLORS.badge,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
cardText: {
flex: 1,
},
cardName: {
fontSize: 18,
lineHeight: 24,
fontWeight: "500",
letterSpacing: -0.3,
color: COLORS.body,
},
cardCaption: {
marginTop: 2,
fontSize: 15,
lineHeight: 20,
color: COLORS.muted,
},
amount: {
fontSize: 18,
lineHeight: 24,
fontWeight: "500",
letterSpacing: -0.3,
color: COLORS.body,
},
divider: {
marginTop: 14,
left: 60,
height: StyleSheet.hairlineWidth,
backgroundColor: COLORS.divider,
},
footnote: {
alignSelf: "stretch",
marginTop: 16,
fontSize: 15,
lineHeight: 21,
color: COLORS.muted,
textAlign: "center",
},
action: {
alignSelf: "stretch",
marginTop: 26,
height: 56,
borderRadius: 12,
backgroundColor: COLORS.action,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
actionPressed: {
backgroundColor: COLORS.actionPressed,
},
actionLabel: {
fontSize: 17,
fontWeight: "700",
letterSpacing: -0.3,
color: COLORS.actionLabel,
},
});
export { ApplePayBottomSheet };
export type { IApplePayBottomSheet, IGlyph } from "./types";
export default memo(ApplePayBottomSheet);export const DEFAULT_BRAND_LABEL = "Pay";
export const DEFAULT_DESCRIPTION =
"Apple Pay – convenient, secure and confidential way of payments";
export const DEFAULT_CARD_NAME = "Visa Classic";
export const DEFAULT_CARD_CAPTION = "•••• 2244 • add";
export const DEFAULT_AMOUNT = "$1275.60";
export const DEFAULT_FOOTNOTE =
"Once connected, you can use your card using iPhone, Apple Watch, iPad and Mac";
export const DEFAULT_ACTION_LABEL = "Add in Apple Wallet";
export const SHEET_CORNER_RADIUS = 18;
export const BACKDROP_OPACITY = 0.4;
export const CONTENT_BOTTOM_PADDING = 12;
export const CONTENT_HORIZONTAL_PADDING = 22;
export const APPLE_VIEW_BOX = "-1.5 0 20 20";
export const APPLE_GROUP_TRANSFORM = "translate(-46,-7279)";
export const APPLE_PATH =
"M57.5708873,7282.19296 C58.2999598,7281.34797 58.7914012,7280.17098 58.6569121,7279 C57.6062792,7279.04 56.3352055,7279.67099 55.5818643,7280.51498 C54.905374,7281.26397 54.3148354,7282.46095 54.4735932,7283.60894 C55.6455696,7283.69593 56.8418148,7283.03894 57.5708873,7282.19296 M60.1989864,7289.62485 C60.2283111,7292.65181 62.9696641,7293.65879 63,7293.67179 C62.9777537,7293.74279 62.562152,7295.10677 61.5560117,7296.51675 C60.6853718,7297.73474 59.7823735,7298.94772 58.3596204,7298.97372 C56.9621472,7298.99872 56.5121648,7298.17973 54.9134635,7298.17973 C53.3157735,7298.17973 52.8162425,7298.94772 51.4935978,7298.99872 C50.1203933,7299.04772 49.0738052,7297.68074 48.197098,7296.46676 C46.4032359,7293.98379 45.0330649,7289.44985 46.8734421,7286.3899 C47.7875635,7284.87092 49.4206455,7283.90793 51.1942837,7283.88393 C52.5422083,7283.85893 53.8153044,7284.75292 54.6394294,7284.75292 C55.4635543,7284.75292 57.0106846,7283.67793 58.6366882,7283.83593 C59.3172232,7283.86293 61.2283842,7284.09893 62.4549652,7285.8199 C62.355868,7285.8789 60.1747177,7287.09489 60.1989864,7289.62485";
export const VISA_VIEW_BOX = "0 0 32 32";
export const VISA_PATH =
"M15.854 11.329l-2.003 9.367h-2.424l2.006-9.367zM26.051 17.377l1.275-3.518 0.735 3.518zM28.754 20.696h2.242l-1.956-9.367h-2.069c-0.003-0-0.007-0-0.010-0-0.459 0-0.853 0.281-1.019 0.68l-0.003 0.007-3.635 8.68h2.544l0.506-1.4h3.109zM22.429 17.638c0.010-2.473-3.419-2.609-3.395-3.714 0.008-0.336 0.327-0.694 1.027-0.785 0.13-0.013 0.28-0.021 0.432-0.021 0.711 0 1.385 0.162 1.985 0.452l-0.027-0.012 0.425-1.987c-0.673-0.261-1.452-0.413-2.266-0.416h-0.001c-2.396 0-4.081 1.275-4.096 3.098-0.015 1.348 1.203 2.099 2.122 2.549 0.945 0.459 1.262 0.754 1.257 1.163-0.006 0.63-0.752 0.906-1.45 0.917-0.032 0.001-0.071 0.001-0.109 0.001-0.871 0-1.691-0.219-2.407-0.606l0.027 0.013-0.439 2.052c0.786 0.315 1.697 0.497 2.651 0.497 0.015 0 0.030-0 0.045-0h-0.002c2.546 0 4.211-1.257 4.22-3.204zM12.391 11.329l-3.926 9.367h-2.562l-1.932-7.477c-0.037-0.364-0.26-0.668-0.57-0.82l-0.006-0.003c-0.688-0.338-1.488-0.613-2.325-0.786l-0.066-0.011 0.058-0.271h4.124c0 0 0.001 0 0.001 0 0.562 0 1.028 0.411 1.115 0.948l0.001 0.006 1.021 5.421 2.522-6.376z";
export const COLORS = {
sheet: "#ffffff",
ink: "#000000",
body: "#1c1c1e",
muted: "#8e8e93",
badge: "#f1f1f4",
badgeMark: "#1a1f71",
divider: "#e5e5ea",
action: "#000000",
actionPressed: "#2c2c2e",
actionLabel: "#ffffff",
rowPressed: "#f7f7f9",
} as const;export interface IGlyph {
size?: number;
color?: string;
}
export interface IApplePayBottomSheet {
brandLabel?: string;
description?: string;
cardName?: string;
cardCaption?: string;
amount?: string;
footnote?: string;
actionLabel?: string;
openOnFocus?: boolean;
onCardPress?: () => void;
onActionPress?: () => void;
onClose?: () => void;
}