
Шторки, всплывающие над затемнением — формы, акции, оплата и вход.
npx astra-rn add signup-v1pnpm dlx astra-rn add signup-v1yarn dlx astra-rn add signup-v1bunx --bun astra-rn add signup-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 {
BottomSheetBackdrop,
BottomSheetModal,
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 { IGlyph, ISignUpBottomSheet } from "./types";
import {
APPLE_PATH,
APPLE_VIEW_BOX,
BACKDROP_OPACITY,
BUTTON_GAP,
BUTTON_HEIGHT,
BUTTON_RADIUS,
CLOSE_SIZE,
COLORS,
CONTENT_BOTTOM_PADDING,
CONTENT_HORIZONTAL_PADDING,
CONTENT_TOP_PADDING,
DEFAULT_DESCRIPTION,
DEFAULT_PRIMARY_LABEL,
DEFAULT_SECONDARY_LABEL,
DEFAULT_TITLE,
GOOGLE_GROUP_TRANSFORM,
GOOGLE_PATH,
GOOGLE_VIEW_BOX,
SHEET_BOTTOM_GAP,
SHEET_CORNER_RADIUS,
SHEET_HORIZONTAL_MARGIN,
SPARKLE_PATH,
SPARKLE_VIEW_BOX,
TILE_RADIUS,
TILE_SIZE,
} from "./const";
const SparkleGlyph: React.FC<IGlyph> = ({
size = 32,
color = COLORS.ink,
}: IGlyph) => (
<Svg width={size} height={size} viewBox={SPARKLE_VIEW_BOX}>
<Path fill={color} d={SPARKLE_PATH} />
</Svg>
);
const AppleGlyph: React.FC<IGlyph> = ({
size = 22,
color = COLORS.ink,
}: IGlyph) => (
<Svg width={size} height={size} viewBox={APPLE_VIEW_BOX}>
<Path fill={color} d={APPLE_PATH} />
</Svg>
);
const GoogleGlyph: React.FC<IGlyph> = ({
size = 18,
color = COLORS.ink,
}: IGlyph) => (
<Svg width={size} height={size} viewBox={GOOGLE_VIEW_BOX}>
<G transform={GOOGLE_GROUP_TRANSFORM}>
<Path fill={color} d={GOOGLE_PATH} />
</G>
</Svg>
);
const CloseGlyph: React.FC<IGlyph> = ({
size = 20,
color = COLORS.closeMark,
}: IGlyph) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M6 6L18 18M18 6L6 18"
stroke={color}
strokeWidth={2.4}
strokeLinecap="round"
/>
</Svg>
);
const SignUpBottomSheet: React.FC<ISignUpBottomSheet> = ({
title = DEFAULT_TITLE,
description = DEFAULT_DESCRIPTION,
primaryLabel = DEFAULT_PRIMARY_LABEL,
secondaryLabel = DEFAULT_SECONDARY_LABEL,
hideProviders = false,
hideCloseButton = false,
openOnFocus = true,
onPrimaryPress,
onSecondaryPress,
onApplePress,
onGooglePress,
onClose,
}: ISignUpBottomSheet) => {
const sheetRef = useRef<BottomSheetModal>(null);
const insets = useSafeAreaInsets();
const [pressedKey, setPressedKey] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
if (openOnFocus) sheetRef.current?.present();
return () => sheetRef.current?.dismiss();
}, [openOnFocus]),
);
const handleClose = useCallback((): void => {
sheetRef.current?.dismiss();
}, []);
const handlePressOut = useCallback((): void => setPressedKey(null), []);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
opacity={BACKDROP_OPACITY}
pressBehavior="close"
/>
),
[],
);
const bottomInset = useMemo(
() => insets.bottom - 16 + SHEET_BOTTOM_GAP,
[insets.bottom],
);
return (
<BottomSheetModal
ref={sheetRef}
detached
bottomInset={bottomInset}
enablePanDownToClose
enableDynamicSizing
onDismiss={onClose}
backdropComponent={renderBackdrop}
handleComponent={null}
backgroundStyle={styles.background}
style={styles.sheet}
>
<BottomSheetView style={styles.content}>
<View style={styles.header}>
<View style={styles.tile}>
<SparkleGlyph />
</View>
{hideCloseButton ? null : (
<Pressable
accessibilityRole="button"
accessibilityLabel="Close"
hitSlop={12}
onPress={handleClose}
onPressIn={() => setPressedKey("close")}
onPressOut={handlePressOut}
style={[
styles.close,
pressedKey === "close" ? styles.closePressed : null,
]}
>
<CloseGlyph />
</Pressable>
)}
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={primaryLabel}
onPress={onPrimaryPress}
onPressIn={() => setPressedKey("primary")}
onPressOut={handlePressOut}
style={[
styles.primary,
pressedKey === "primary" ? styles.primaryPressed : null,
]}
>
<Text style={styles.primaryLabel} numberOfLines={1}>
{primaryLabel}
</Text>
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={secondaryLabel}
onPress={onSecondaryPress}
onPressIn={() => setPressedKey("secondary")}
onPressOut={handlePressOut}
style={[
styles.secondary,
pressedKey === "secondary" ? styles.surfacePressed : null,
]}
>
<Text style={styles.secondaryLabel} numberOfLines={1}>
{secondaryLabel}
</Text>
</Pressable>
{hideProviders ? null : (
<View style={styles.providers}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Continue with Apple"
onPress={onApplePress}
onPressIn={() => setPressedKey("apple")}
onPressOut={handlePressOut}
style={[
styles.provider,
pressedKey === "apple" ? styles.surfacePressed : null,
]}
>
<AppleGlyph />
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Continue with Google"
onPress={onGooglePress}
onPressIn={() => setPressedKey("google")}
onPressOut={handlePressOut}
style={[
styles.provider,
pressedKey === "google" ? styles.surfacePressed : null,
]}
>
<GoogleGlyph />
</Pressable>
</View>
)}
</BottomSheetView>
</BottomSheetModal>
);
};
const styles = StyleSheet.create({
sheet: {
marginHorizontal: SHEET_HORIZONTAL_MARGIN,
shadowColor: "#000000",
shadowOpacity: 0.16,
shadowRadius: 28,
shadowOffset: { width: 0, height: 8 },
elevation: 16,
},
background: {
backgroundColor: COLORS.sheet,
borderRadius: SHEET_CORNER_RADIUS,
},
content: {
alignSelf: "stretch",
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
paddingTop: CONTENT_TOP_PADDING,
paddingBottom: CONTENT_BOTTOM_PADDING,
},
header: {
flexDirection: "row",
alignItems: "flex-start",
justifyContent: "space-between",
},
tile: {
width: TILE_SIZE,
height: TILE_SIZE,
borderRadius: 90,
backgroundColor: COLORS.tile,
alignItems: "center",
justifyContent: "center",
},
close: {
width: CLOSE_SIZE,
height: CLOSE_SIZE,
borderRadius: CLOSE_SIZE / 2,
backgroundColor: COLORS.close,
alignItems: "center",
justifyContent: "center",
},
closePressed: {
backgroundColor: COLORS.closePressed,
},
title: {
alignSelf: "stretch",
marginTop: 22,
fontSize: 26,
lineHeight: 32,
fontWeight: "700",
letterSpacing: -0.6,
color: COLORS.ink,
},
description: {
alignSelf: "stretch",
marginTop: 8,
fontSize: 15.5,
lineHeight: 21,
color: COLORS.muted,
},
primary: {
alignSelf: "stretch",
marginTop: 20,
height: BUTTON_HEIGHT,
borderRadius: BUTTON_RADIUS,
backgroundColor: COLORS.primary,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
primaryPressed: {
backgroundColor: COLORS.primaryPressed,
},
primaryLabel: {
fontSize: 17,
fontWeight: "700",
letterSpacing: -0.3,
color: COLORS.primaryLabel,
},
secondary: {
alignSelf: "stretch",
marginTop: BUTTON_GAP,
height: BUTTON_HEIGHT,
borderRadius: BUTTON_RADIUS,
backgroundColor: COLORS.surface,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
secondaryLabel: {
fontSize: 17,
fontWeight: "600",
letterSpacing: -0.3,
color: COLORS.ink,
},
surfacePressed: {
backgroundColor: COLORS.surfacePressed,
},
providers: {
flexDirection: "row",
gap: BUTTON_GAP,
marginTop: BUTTON_GAP,
},
provider: {
flex: 1,
height: BUTTON_HEIGHT,
borderRadius: BUTTON_RADIUS,
backgroundColor: COLORS.surface,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
});
export { SignUpBottomSheet };
export type { IGlyph, ISignUpBottomSheet } from "./types";
export default memo(SignUpBottomSheet);export const DEFAULT_TITLE = "Get Started";
export const DEFAULT_DESCRIPTION =
"Register for events, subscribe to calendars and manage events you're going to.";
export const DEFAULT_PRIMARY_LABEL = "Continue with Phone";
export const DEFAULT_SECONDARY_LABEL = "Continue with Email";
export const SHEET_CORNER_RADIUS = 40;
export const BACKDROP_OPACITY = 0.4;
export const SHEET_HORIZONTAL_MARGIN = 10;
export const SHEET_BOTTOM_GAP = 1;
export const CONTENT_BOTTOM_PADDING = 32;
export const CONTENT_HORIZONTAL_PADDING = 22;
export const CONTENT_TOP_PADDING = 32;
export const TILE_SIZE = 55;
export const TILE_RADIUS = 18;
export const CLOSE_SIZE = 30;
export const BUTTON_HEIGHT = 54;
export const BUTTON_RADIUS = 14;
export const BUTTON_GAP = 10;
export const SPARKLE_VIEW_BOX = "0 0 133 134";
export const SPARKLE_PATH =
"M133 67C96.282 67 66.5 36.994 66.5 0c0 36.994-29.782 67-66.5 67 36.718 0 66.5 30.006 66.5 67 0-36.994 29.782-67 66.5-67";
export const APPLE_VIEW_BOX = "0 0 24 24";
export const APPLE_PATH =
"M18.71 19.5C17.88 20.74 17 21.95 15.66 21.97C14.32 22 13.89 21.18 12.37 21.18C10.84 21.18 10.37 21.95 9.09997 22C7.78997 22.05 6.79997 20.68 5.95997 19.47C4.24997 17 2.93997 12.45 4.69997 9.39C5.56997 7.87 7.12997 6.91 8.81997 6.88C10.1 6.86 11.32 7.75 12.11 7.75C12.89 7.75 14.37 6.68 15.92 6.84C16.57 6.87 18.39 7.1 19.56 8.82C19.47 8.88 17.39 10.1 17.41 12.63C17.44 15.65 20.06 16.66 20.09 16.67C20.06 16.74 19.67 18.11 18.71 19.5ZM13 3.5C13.73 2.67 14.94 2.04 15.94 2C16.07 3.17 15.6 4.35 14.9 5.19C14.21 6.04 13.07 6.7 11.95 6.61C11.8 5.46 12.36 4.26 13 3.5Z";
export const GOOGLE_VIEW_BOX = "0 0 20 20";
export const GOOGLE_GROUP_TRANSFORM = "translate(-244,-7239)";
export const GOOGLE_PATH =
"M263.821537,7247.00386 L254.211298,7247.00386 C254.211298,7248.0033 254.211298,7250.00218 254.205172,7251.00161 L259.774046,7251.00161 C259.560644,7252.00105 258.804036,7253.40026 257.734984,7254.10487 C257.733963,7254.10387 257.732942,7254.11086 257.7309,7254.10986 C256.309581,7255.04834 254.43389,7255.26122 253.041161,7254.98137 C250.85813,7254.54762 249.130492,7252.96451 248.429023,7250.95364 C248.433107,7250.95064 248.43617,7250.92266 248.439233,7250.92066 C248.000176,7249.67336 248.000176,7248.0033 248.439233,7247.00386 L248.438212,7247.00386 C249.003881,7245.1669 250.783592,7243.49084 252.969687,7243.0321 C254.727956,7242.65931 256.71188,7243.06308 258.170978,7244.42831 C258.36498,7244.23842 260.856372,7241.80579 261.043226,7241.6079 C256.0584,7237.09344 248.076756,7238.68155 245.090149,7244.51127 L245.089128,7244.51127 C245.089128,7244.51127 245.090149,7244.51127 245.084023,7244.52226 L245.084023,7244.52226 C243.606545,7247.38565 243.667809,7250.75975 245.094233,7253.48622 C245.090149,7253.48921 245.087086,7253.49121 245.084023,7253.49421 C246.376687,7256.0028 248.729215,7257.92672 251.563684,7258.6593 C254.574796,7259.44886 258.406843,7258.90916 260.973794,7256.58747 C260.974815,7256.58847 260.975836,7256.58947 260.976857,7256.59047 C263.15172,7254.63157 264.505648,7251.29445 263.821537,7247.00386";
export const COLORS = {
sheet: "#ffffff",
ink: "#111111",
muted: "#8a8a8e",
tile: "#f2f2f2",
surface: "#efefef",
surfacePressed: "#e4e4e4",
close: "#ededed",
closePressed: "#e0e0e0",
closeMark: "#9a9a9e",
primary: "#131313",
primaryPressed: "#333333",
primaryLabel: "#ffffff",
} as const;export interface IGlyph {
size?: number;
color?: string;
}
export interface ISignUpBottomSheet {
title?: string;
description?: string;
primaryLabel?: string;
secondaryLabel?: string;
hideProviders?: boolean;
hideCloseButton?: boolean;
openOnFocus?: boolean;
onPrimaryPress?: () => void;
onSecondaryPress?: () => void;
onApplePress?: () => void;
onGooglePress?: () => void;
onClose?: () => void;
}