# Social-first hero

> A social-first sign-in Screen: a dark brand hero (logo, headline, subtitle, centred) above a panel where Apple and Google lead and "Continue with email" reveals the email form.

Block · auth. Registry item `sign-in-02`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/sign-in-02.json

## Installation

```sh
npx nativecn-cli@latest add sign-in-02 --route "(auth)/sign-in"
```

- `--route "(auth)/sign-in"` is the Block's suggested route: change it to put the Screen somewhere else in your app folder.
- In a project with the feature Structure, also pass `--feature <name>` (for example `--feature auth`).
- `add` never overwrites an existing route file.

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [container](https://nativecn.dev/docs/components/container), [text](https://nativecn.dev/docs/components/text), [button](https://nativecn.dev/docs/components/button), [form-field](https://nativecn.dev/docs/components/form-field), [input](https://nativecn.dev/docs/components/input), [focus-chain](https://nativecn.dev/docs/primitives/focus-chain), [icon](https://nativecn.dev/docs/components/icon), [separator](https://nativecn.dev/docs/components/separator), [toast](https://nativecn.dev/docs/components/toast).

Packages: `react-hook-form@^7.89.0`, `zod@^4.6.5`, `@hookform/resolvers@^5.9.1`, `lucide-react-native@^1.51.0`, `react-native-svg`, `react-native-safe-area-context`.

## What makes it different

Dark brand hero, centred; Apple/Google are the primary path; "Continue with email" reveals the fields.

## Block Variants

- [sign-in-01](https://nativecn.dev/docs/blocks/sign-in-01) – Classic centred: Logo, email and password, "or", then social; everything on one screen.
- **sign-in-02** (this page) – Social-first hero: Dark brand hero, centred; Apple/Google are the primary path; "Continue with email" reveals the fields.
- [sign-in-03](https://nativecn.dev/docs/blocks/sign-in-03) – Email-first, two steps: Email → password, one question per step, with "Email me a code instead" (InputOTP).

## Props

| Prop | Type and description |
|---|---|
| `onSubmit` | (values: { email, password }) => void \| Promise<void>: sign in; throw to show the error as a toast, resolve for a "Signed in" toast |
| `onSocialSignIn` | (provider: "apple" \| "google") => void \| Promise<void>: run that provider's sign-in; a thrown error becomes a toast |

## Accessibility

- The headline has role heading; the logo icon and the lines around "or" are decorative.
- Each field is in a FormField: its label is the field's accessible name, and a validation error becomes its hint and is announced once.
- Errors from your callbacks and the success message are toast()s, which screen readers announce.
- The submit Button is announced busy while your callback runs.
- Apple and Google are labelled Buttons ("Continue with Apple", "Continue with Google"); their logos are decorative.

## Notes

Render it from a route: `export default function Screen() { return <SignIn02 onSocialSignIn={signInWith} onSubmit={signIn} />; }`. The hero is drawn with the dark Scheme's Colour Roles in both Schemes (a nested ThemeProvider scheme="dark") and runs under a light status bar. The email form appears only after "Continue with email"; it is validated with zod through react-hook-form, in a FocusChain. Validation errors appear under each field. Errors thrown by your callbacks, and the success message, appear only as toast()s: render <Toaster /> in the root Layout (init and create already do). The Block never talks to a server: wire your auth backend in the route file through the props, and keep any session or token in expo-secure-store, never AsyncStorage.

## Source

### {screens}/sign-in-02/index.tsx

```tsx
import { zodResolver } from "@hookform/resolvers/zod";
import { Hexagon } from "lucide-react-native";
import { use, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { StatusBar, View } from "react-native";
import { SafeAreaInsetsContext, initialWindowMetrics } from "react-native-safe-area-context";
import { z } from "zod";

import { Button } from "@/components/button";
import { Container } from "@/components/container";
import { FormField } from "@/components/form-field";
import { Icon } from "@/components/icon";
import { Input } from "@/components/input";
import { FocusChain } from "@/components/primitives/focus-chain";
import { Separator } from "@/components/separator";
import { Text } from "@/components/text";
import { toast } from "@/components/toast";
import { ThemeProvider, createStyles, useTheme } from "@/theme";

import { SocialButtons, type SocialProvider } from "./components/social-buttons";

export type { SocialProvider } from "./components/social-buttons";

// sign-in-02, social-first hero: a dark brand hero with the logo, headline and subtitle centred;
// Apple and Google are the main path, and "Continue with email" reveals the email form.
// Edit the copy, the logo and the rules below; they are yours.

const schema = z.object({
  email: z.string().trim().min(1, "Enter your email").pipe(z.email("Enter a valid email")),
  password: z.string().min(1, "Password is required"),
});

export type SignIn02Values = z.infer<typeof schema>;

export type SignIn02Props = {
  /**
   * Sign the user in with your auth backend. Throw to show the error as a toast; resolve and a
   * "Signed in" toast appears (navigate away here). Store any session in expo-secure-store.
   */
  onSubmit?: (values: SignIn02Values) => void | Promise<void>;
  /** Run the provider's sign-in. Throw to show the error as a toast. */
  onSocialSignIn?: (provider: SocialProvider) => void | Promise<void>;
};

export function SignIn02({ onSubmit, onSocialSignIn }: SignIn02Props) {
  const styles = useStyles();
  const [emailOpen, setEmailOpen] = useState(false);
  const {
    control,
    handleSubmit,
    formState: { isSubmitting },
  } = useForm<SignIn02Values>({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  });

  const submit = handleSubmit(async (values) => {
    try {
      await onSubmit?.(values);
      toast.success("Signed in");
    } catch (error) {
      toast.error("Couldn't sign in", { description: messageOf(error) });
    }
  });

  const socialSignIn = async (provider: SocialProvider) => {
    try {
      await onSocialSignIn?.(provider);
    } catch (error) {
      toast.error("Couldn't sign in", { description: messageOf(error) });
    }
  };

  return (
    <Container edges={["bottom"]} padded={false} contentContainerStyle={styles.content}>
      {/* The hero is dark in both Schemes, so the status bar above it is light. */}
      <StatusBar barStyle="light-content" />
      <ThemeProvider scheme="dark">
        <Hero />
      </ThemeProvider>

      <View style={styles.panel}>
        <SocialButtons size="lg" onPress={socialSignIn} disabled={isSubmitting} />

        {emailOpen ? (
          <>
            <View style={styles.divider}>
              <Separator style={styles.line} />
              <Text variant="caption" color="mutedForeground">
                or
              </Text>
              <Separator style={styles.line} />
            </View>
            <FocusChain onSubmit={submit}>
              <Controller
                control={control}
                name="email"
                render={({ field, fieldState }) => (
                  <FormField label="Email" error={fieldState.error?.message}>
                    <Input
                      ref={field.ref}
                      value={field.value}
                      onChangeText={field.onChange}
                      onBlur={field.onBlur}
                      autoFocus
                      placeholder="you@example.com"
                      keyboardType="email-address"
                      autoCapitalize="none"
                      autoComplete="email"
                      textContentType="emailAddress"
                    />
                  </FormField>
                )}
              />
              <Controller
                control={control}
                name="password"
                render={({ field, fieldState }) => (
                  <FormField label="Password" error={fieldState.error?.message}>
                    <Input
                      ref={field.ref}
                      value={field.value}
                      onChangeText={field.onChange}
                      onBlur={field.onBlur}
                      secureTextEntry
                      autoComplete="current-password"
                      textContentType="password"
                    />
                  </FormField>
                )}
              />
            </FocusChain>
            <Button size="lg" label="Sign in" loading={isSubmitting} onPress={submit} />
          </>
        ) : (
          <Button
            variant="ghost"
            size="lg"
            label="Continue with email"
            onPress={() => setEmailOpen(true)}
          />
        )}

        <Text variant="caption" color="mutedForeground" align="center" style={styles.terms}>
          By continuing you agree to the Terms and Privacy Policy.
        </Text>
      </View>
    </Container>
  );
}

/** The brand hero, rendered inside a dark ThemeProvider so every Colour Role is the dark one. */
function Hero() {
  const styles = useHeroStyles();
  const { spacing } = useTheme();
  const insets = use(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets;
  return (
    // The hero runs under the status bar, so its top padding includes the status bar inset.
    <View style={[styles.hero, { paddingTop: (insets?.top ?? 0) + spacing[10] }]}>
      {/* Your logo goes here. */}
      <View style={styles.logo}>
        <Icon icon={Hexagon} size="lg" color="primaryForeground" />
      </View>
      <Text variant="h1" align="center">
        {"Your app,\neverywhere."}
      </Text>
      <Text variant="body" color="mutedForeground" align="center">
        Sign in to pick up where you left off.
      </Text>
    </View>
  );
}

const useStyles = createStyles((t) => ({
  content: { gap: 0 },
  panel: {
    marginTop: -t.spacing[5],
    gap: t.spacing[3],
    paddingHorizontal: t.spacing[6],
    paddingTop: t.spacing[6],
    paddingBottom: t.spacing[8],
    borderTopLeftRadius: t.radius["2xl"],
    borderTopRightRadius: t.radius["2xl"],
    borderCurve: "continuous",
    backgroundColor: t.colors.background,
  },
  divider: {
    flexDirection: "row",
    alignItems: "center",
    gap: t.spacing[3],
    marginVertical: t.spacing[2],
  },
  line: { flex: 1 },
  terms: { marginTop: t.spacing[2] },
}));

const useHeroStyles = createStyles((t) => ({
  hero: {
    flexGrow: 1,
    minHeight: t.scaleValue(360),
    alignItems: "center",
    justifyContent: "center",
    gap: t.spacing[3],
    paddingHorizontal: t.spacing[6],
    // Room for the panel's rounded top, which overlaps the hero.
    paddingBottom: t.spacing[10],
    backgroundColor: t.colors.background,
  },
  logo: {
    width: t.scaleValue(64),
    height: t.scaleValue(64),
    marginBottom: t.spacing[2],
    alignItems: "center",
    justifyContent: "center",
    borderRadius: t.radius["2xl"],
    borderCurve: "continuous",
    backgroundColor: t.colors.primary,
  },
}));

/** The message to show for an error thrown by your callbacks. */
function messageOf(error: unknown): string {
  return error instanceof Error && error.message
    ? error.message
    : "Something went wrong. Try again.";
}
```

### {screens}/sign-in-02/components/social-buttons.tsx

```tsx
import type { LucideIcon, LucideProps } from "lucide-react-native";
import { forwardRef } from "react";
import { View } from "react-native";
import Svg, { Path } from "react-native-svg";

import { Button, type ButtonSize } from "@/components/button";
import { createStyles } from "@/theme";

export type SocialProvider = "apple" | "google";

// Brand logos, drawn to each brand's guidelines and shaped like a Lucide icon so Button can
// size and colour them. Apple's mark takes the text colour (black on light, white on dark);
// Google's "G" always keeps its four brand colours.

/** The Apple logo (Simple Icons, CC0), in the colour Button gives it. */
export const AppleLogo: LucideIcon = forwardRef<Svg, LucideProps>(function AppleLogo(
  { size = 24, color = "currentColor" },
  ref,
) {
  return (
    <Svg ref={ref} width={size} height={size} viewBox="0 0 24 24">
      <Path
        fill={color}
        d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
      />
    </Svg>
  );
});

/** Google's four-colour "G" (its colours are part of the brand, so they never follow the Theme). */
export const GoogleLogo: LucideIcon = forwardRef<Svg, LucideProps>(function GoogleLogo(
  { size = 24 },
  ref,
) {
  return (
    <Svg ref={ref} width={size} height={size} viewBox="0 0 48 48">
      <Path
        fill="#EA4335"
        d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
      />
      <Path
        fill="#4285F4"
        d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
      />
      <Path
        fill="#FBBC05"
        d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
      />
      <Path
        fill="#34A853"
        d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
      />
    </Svg>
  );
});

const providers = [
  { provider: "apple", label: "Continue with Apple", icon: AppleLogo },
  { provider: "google", label: "Continue with Google", icon: GoogleLogo },
] as const satisfies { provider: SocialProvider; label: string; icon: LucideIcon }[];

const useStyles = createStyles((t) => ({
  root: { gap: t.spacing[3] },
}));

export type SocialButtonsProps = {
  /** Called with the provider; your code runs that provider's sign-in. */
  onPress: (provider: SocialProvider) => void;
  /** Default `md`. */
  size?: ButtonSize;
  disabled?: boolean;
};

/** "Continue with Apple" and "Continue with Google", as outline Buttons. */
export function SocialButtons({ onPress, size = "md", disabled = false }: SocialButtonsProps) {
  const styles = useStyles();
  return (
    <View style={styles.root}>
      {providers.map(({ provider, label, icon }) => (
        <Button
          key={provider}
          variant="outline"
          size={size}
          label={label}
          icon={icon}
          disabled={disabled}
          onPress={() => onPress(provider)}
        />
      ))}
    </View>
  );
}
```
