
Шторки, всплывающие над затемнением — формы, акции, оплата и вход.
npx astra-rn add email-verification-v1pnpm dlx astra-rn add email-verification-v1yarn dlx astra-rn add email-verification-v1bunx --bun astra-rn add email-verification-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, { Path } from "react-native-svg";
import type { IEmailIcon, IEmailVerificationBottomSheet } from "./types";
import {
BACKDROP_OPACITY,
COLORS,
CONTENT_BOTTOM_PADDING,
CONTENT_HORIZONTAL_PADDING,
DEFAULT_ACTION_LABEL,
DEFAULT_DESCRIPTION,
DEFAULT_EMAIL,
DEFAULT_SECONDARY_LABEL,
DEFAULT_TITLE,
ENVELOPE_BODY_PATH,
ENVELOPE_FLAP_PATH,
ENVELOPE_VIEW_BOX,
SHEET_CORNER_RADIUS,
} from "./const";
const EmailIcon: React.FC<IEmailIcon> = ({
size = 22,
color = COLORS.envelope,
}: IEmailIcon) => (
<Svg width={size} height={size} viewBox={ENVELOPE_VIEW_BOX}>
<Path fill={color} d={ENVELOPE_BODY_PATH} />
<Path fill={color} d={ENVELOPE_FLAP_PATH} />
</Svg>
);
const EmailVerificationBottomSheet: React.FC<IEmailVerificationBottomSheet> = ({
title = DEFAULT_TITLE,
description = DEFAULT_DESCRIPTION,
email = DEFAULT_EMAIL,
actionLabel = DEFAULT_ACTION_LABEL,
secondaryLabel = DEFAULT_SECONDARY_LABEL,
hideSecondaryAction = false,
openOnFocus = true,
onActionPress,
onSecondaryPress,
onClose,
}: IEmailVerificationBottomSheet) => {
const sheetRef = useRef<BottomSheet>(null);
const insets = useSafeAreaInsets();
const [isMounted, setIsMounted] = useState<boolean>(openOnFocus);
const [isActionPressed, setIsActionPressed] = useState<boolean>(false);
const [isSecondaryPressed, setIsSecondaryPressed] = 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 handleActionPressIn = useCallback(
(): void => setIsActionPressed(true),
[],
);
const handleActionPressOut = useCallback(
(): void => setIsActionPressed(false),
[],
);
const handleSecondaryPressIn = useCallback(
(): void => setIsSecondaryPressed(true),
[],
);
const handleSecondaryPressOut = useCallback(
(): void => setIsSecondaryPressed(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}
handleIndicatorStyle={styles.handleIndicator}
backgroundStyle={styles.background}
style={styles.sheet}
>
<BottomSheetView style={contentStyle}>
<View style={styles.header}>
<View style={styles.tile}>
<EmailIcon />
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
{email ? <Text style={styles.email}>{email}</Text> : null}
</View>
<View style={styles.divider} />
<View style={styles.actions}>
<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>
{hideSecondaryAction ? null : (
<Pressable
accessibilityRole="button"
accessibilityLabel={secondaryLabel}
onPress={onSecondaryPress}
onPressIn={handleSecondaryPressIn}
onPressOut={handleSecondaryPressOut}
style={[
styles.secondary,
isSecondaryPressed ? styles.secondaryPressed : null,
]}
>
<Text style={styles.secondaryLabel} numberOfLines={1}>
{secondaryLabel}
</Text>
</Pressable>
)}
</View>
</BottomSheetView>
</BottomSheet>
);
};
const styles = StyleSheet.create({
sheet: {
shadowColor: "#000000",
shadowOpacity: 0.16,
shadowRadius: 24,
shadowOffset: { width: 0, height: -6 },
elevation: 16,
},
background: {
backgroundColor: COLORS.sheet,
borderTopLeftRadius: SHEET_CORNER_RADIUS,
borderTopRightRadius: SHEET_CORNER_RADIUS,
},
handleIndicator: {
backgroundColor: COLORS.handle,
width: 36,
height: 5,
},
content: {
alignSelf: "stretch",
paddingTop: 24,
},
header: {
alignItems: "center",
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
paddingBottom: 26,
},
tile: {
width: 46,
height: 46,
borderRadius: 14,
borderWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.tileBorder,
backgroundColor: COLORS.tile,
alignItems: "center",
justifyContent: "center",
shadowColor: "#000000",
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 3 },
elevation: 3,
},
title: {
marginTop: 18,
fontSize: 22,
lineHeight: 28,
fontWeight: "700",
letterSpacing: -0.4,
color: COLORS.ink,
textAlign: "center",
},
description: {
marginTop: 10,
fontSize: 15,
lineHeight: 21,
color: COLORS.muted,
textAlign: "center",
},
email: {
marginTop: 2,
fontSize: 15,
lineHeight: 21,
color: COLORS.ink,
textAlign: "center",
},
divider: {
alignSelf: "stretch",
height: StyleSheet.hairlineWidth,
backgroundColor: COLORS.divider,
},
actions: {
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
paddingTop: 22,
},
action: {
alignSelf: "stretch",
height: 56,
borderRadius: 28,
backgroundColor: COLORS.action,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
actionPressed: {
backgroundColor: COLORS.actionPressed,
},
actionLabel: {
fontSize: 16,
fontWeight: "700",
letterSpacing: -0.2,
color: COLORS.actionLabel,
},
secondary: {
alignSelf: "stretch",
marginTop: 8,
height: 50,
borderRadius: 25,
alignItems: "center",
justifyContent: "center",
},
secondaryPressed: {
backgroundColor: COLORS.secondaryPressed,
},
secondaryLabel: {
fontSize: 15,
fontWeight: "600",
letterSpacing: -0.2,
color: COLORS.ink,
},
});
export { EmailVerificationBottomSheet };
export type { IEmailIcon, IEmailVerificationBottomSheet } from "./types";
export default memo(EmailVerificationBottomSheet);export const DEFAULT_TITLE = "Check your email";
export const DEFAULT_DESCRIPTION = "We've sent a link to your email:";
export const DEFAULT_EMAIL = "studio@oukline.com";
export const DEFAULT_ACTION_LABEL = "Open email app";
export const DEFAULT_SECONDARY_LABEL = "Resend email";
export const SHEET_CORNER_RADIUS = 42;
export const BACKDROP_OPACITY = 0.3;
export const CONTENT_BOTTOM_PADDING = 8;
export const CONTENT_HORIZONTAL_PADDING = 24;
export const ENVELOPE_VIEW_BOX = "0 0 32 32";
export const ENVELOPE_BODY_PATH =
"M 1.9707031,13 C 1.4299152,13.015903 0.99982032,13.458978 1,14 L 1.0117187,30.523438 14.25,20.685547 2.5410156,13.158203 C 2.3712265,13.049137 2.1724146,12.993988 1.9707031,13 Z M 29.970703,13 C 29.788919,13.0052 29.612,13.05992 29.458984,13.158203 L 17.75,20.685547 31,30.523438 V 14 c 1.84e-4,-0.563907 -0.465631,-1.016463 -1.029297,-1 z M 15.146484,21.261719 2.1660156,31 H 29.832031 L 16.849609,21.265625 16,21.810547 Z";
export const ENVELOPE_FLAP_PATH =
"M 15.986328 1.0039062 C 15.751643 1.0080638 15.52191 1.0908076 15.347656 1.2402344 L 2.7089844 12.078125 L 16 20.623047 L 29.291016 12.078125 L 16.654297 1.2402344 C 16.460649 1.0741774 16.221013 0.99974869 15.986328 1.0039062 z";
export const COLORS = {
sheet: "#ffffff",
ink: "#16181d",
muted: "#8a8a8e",
divider: "#ececec",
handle: "#d9d9de",
tile: "#ffffff",
tileBorder: "#eaeaea",
envelope: "#1c2f5e",
action: "#111111",
actionPressed: "#2c2c2c",
actionLabel: "#ffffff",
secondaryPressed: "#f4f4f5",
} as const;export interface IEmailIcon {
size?: number;
color?: string;
}
export interface IEmailVerificationBottomSheet {
title?: string;
description?: string;
email?: string;
actionLabel?: string;
secondaryLabel?: string;
hideSecondaryAction?: boolean;
openOnFocus?: boolean;
onActionPress?: () => void;
onSecondaryPress?: () => void;
onClose?: () => void;
}