# Verify Email

A check-your-inbox screen after signup, with open mail and resend actions and a cooldown timer.

Verify Email is an open-source block from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/verify-email

**React**

**Example**

```tsx title="components/blocks/verify-email.tsx"
"use client"

import { CheckIcon, EnvelopeSimpleIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Button } from "@/components/ui/button"

const EMAIL = "jamie@example.com"

export function VerifyEmail() {
  const [verified, setVerified] = React.useState(false)
  const [left, setLeft] = React.useState(0)

  React.useEffect(() => {
    if (left <= 0) return
    const timer = window.setTimeout(() => setLeft(left - 1), 1000)
    return () => window.clearTimeout(timer)
  }, [left])

  if (verified) {
    return (
      <section className="flex min-h-130 items-center justify-center p-6">
        <div className="flex max-w-105 flex-col items-center gap-3.5 text-center transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-4.5 starting:opacity-0 motion-reduce:transition-none">
          <span className="flex size-19 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_25%)] text-white dark:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]">
            <CheckIcon className="size-9.5" />
          </span>
          <h1 className="text-4xl font-semibold tracking-tight">
            Your email is verified.
          </h1>
          <p className="text-lg text-muted-foreground">
            {EMAIL} is now your suiss Account.
          </p>
          <button
            type="button"
            onClick={() => setVerified(false)}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Start over ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="flex min-h-130 items-center justify-center p-6">
      <div className="flex flex-col items-center text-center">
        <div className="relative h-18 w-24">
          <span className="absolute inset-0 flex items-center justify-center rounded-[0.875rem] bg-linear-160 from-cyan-500 to-primary text-white shadow-[0_10px_30px_-8px_color-mix(in_oklab,var(--color-primary)_55%,transparent)]">
            <EnvelopeSimpleIcon className="size-10" />
          </span>
          <span className="absolute -end-2 -top-2 flex size-6.5 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_15%)] text-sm font-semibold text-white ring-3 ring-background dark:bg-red-500">
            1<span className="sr-only">new message</span>
          </span>
        </div>
        <h1 className="mt-6.5 text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Check your inbox.
        </h1>
        <p className="mt-3 max-w-110 text-lg text-muted-foreground">
          We sent a verification link to{" "}
          <strong className="font-semibold text-foreground">{EMAIL}</strong>.
          Tap the link to activate your account.
        </p>
        <div className="mt-7.5 flex flex-wrap justify-center gap-3.5">
          <Button size="lg" onClick={() => setVerified(true)}>
            Open Mail
          </Button>
          <Button
            size="lg"
            variant="outline"
            disabled={left > 0}
            onClick={() => setLeft(45)}
            className="tabular-nums"
          >
            {left > 0 ? `Sent again · ${left}` : "Resend"}
          </Button>
        </div>
        <p className="mt-6.5 flex flex-wrap justify-center gap-x-1 text-sm text-muted-foreground">
          <span>Didn't get it? Check your junk folder or</span>
          <a href="#change" className="text-primary hover:underline">
            change the address ›
          </a>
        </p>
      </div>
    </section>
  )
}
```

A check-your-inbox screen shown after signup that names the address the link went to, with an Open Mail button, a Resend button with a 45-second cooldown and a link to change the address. Use it for email verification, magic links or password reset confirmations.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/verify-email
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks verify-email
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button).

3. Copy the source into `components/blocks/verify-email.tsx`:

```tsx title="components/blocks/verify-email.tsx"
"use client"

import { CheckIcon, EnvelopeSimpleIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Button } from "@/components/ui/button"

const EMAIL = "jamie@example.com"

export function VerifyEmail() {
  const [verified, setVerified] = React.useState(false)
  const [left, setLeft] = React.useState(0)

  React.useEffect(() => {
    if (left <= 0) return
    const timer = window.setTimeout(() => setLeft(left - 1), 1000)
    return () => window.clearTimeout(timer)
  }, [left])

  if (verified) {
    return (
      <section className="flex min-h-130 items-center justify-center p-6">
        <div className="flex max-w-105 flex-col items-center gap-3.5 text-center transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-4.5 starting:opacity-0 motion-reduce:transition-none">
          <span className="flex size-19 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_25%)] text-white dark:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]">
            <CheckIcon className="size-9.5" />
          </span>
          <h1 className="text-4xl font-semibold tracking-tight">
            Your email is verified.
          </h1>
          <p className="text-lg text-muted-foreground">
            {EMAIL} is now your suiss Account.
          </p>
          <button
            type="button"
            onClick={() => setVerified(false)}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Start over ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="flex min-h-130 items-center justify-center p-6">
      <div className="flex flex-col items-center text-center">
        <div className="relative h-18 w-24">
          <span className="absolute inset-0 flex items-center justify-center rounded-[0.875rem] bg-linear-160 from-cyan-500 to-primary text-white shadow-[0_10px_30px_-8px_color-mix(in_oklab,var(--color-primary)_55%,transparent)]">
            <EnvelopeSimpleIcon className="size-10" />
          </span>
          <span className="absolute -end-2 -top-2 flex size-6.5 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_15%)] text-sm font-semibold text-white ring-3 ring-background dark:bg-red-500">
            1<span className="sr-only">new message</span>
          </span>
        </div>
        <h1 className="mt-6.5 text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Check your inbox.
        </h1>
        <p className="mt-3 max-w-110 text-lg text-muted-foreground">
          We sent a verification link to{" "}
          <strong className="font-semibold text-foreground">{EMAIL}</strong>.
          Tap the link to activate your account.
        </p>
        <div className="mt-7.5 flex flex-wrap justify-center gap-3.5">
          <Button size="lg" onClick={() => setVerified(true)}>
            Open Mail
          </Button>
          <Button
            size="lg"
            variant="outline"
            disabled={left > 0}
            onClick={() => setLeft(45)}
            className="tabular-nums"
          >
            {left > 0 ? `Sent again · ${left}` : "Resend"}
          </Button>
        </div>
        <p className="mt-6.5 flex flex-wrap justify-center gap-x-1 text-sm text-muted-foreground">
          <span>Didn't get it? Check your junk folder or</span>
          <a href="#change" className="text-primary hover:underline">
            change the address ›
          </a>
        </p>
      </div>
    </section>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { VerifyEmail } from "@/components/blocks/verify-email"
```

```tsx
<VerifyEmail />
```

The address comes from the `EMAIL` constant at the top of the file; replace it with the signed-in user's address, for example by turning it into a prop. In the demo, Open Mail marks the email as verified, but in production verification happens when the user follows the link, so point that button at a mail app or remove it. Connect Resend to your email service and `#change` to your account settings.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-verify-email--default.light.webp)

```tsx title="uniwind/components/blocks/verify-email.tsx"
import { CheckIcon, EnvelopeSimpleIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, ScrollView, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

const EMAIL = "jamie@example.com"

function VerifyEmail({
  onChangeAddress,
  onOpenMail,
}: {
  onChangeAddress?: () => void
  onOpenMail?: () => void
}) {
  const [verified, setVerified] = React.useState(false)
  const [left, setLeft] = React.useState(0)

  React.useEffect(() => {
    if (left <= 0) return
    const timer = setTimeout(() => setLeft(left - 1), 1000)
    return () => clearTimeout(timer)
  }, [left])

  if (verified) {
    return (
      <Screen>
        <View className="flex-1 items-center justify-center p-6">
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            className="items-center gap-3.5"
          >
            <View className="size-19 items-center justify-center rounded-full bg-green-500">
              <Icon
                as={CheckIcon}
                weight="bold"
                className="size-9.5 text-primary-foreground"
              />
            </View>
            <Text role="heading" className="text-center text-3xl font-semibold">
              Your email is verified.
            </Text>
            <Text className="text-center text-lg text-muted-foreground">
              {EMAIL} is now your suiss Account.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => setVerified(false)}
              className="active:opacity-60"
            >
              <Text className="text-lg text-primary">Start over ›</Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <ScrollView contentContainerClassName="grow items-center justify-center px-6 py-8">
        <View className="h-18 w-24">
          <View className="absolute inset-0 items-center justify-center rounded-[0.875rem] bg-primary shadow-lg">
            <Icon
              as={EnvelopeSimpleIcon}
              className="size-10 text-primary-foreground"
            />
          </View>
          <View
            accessible
            accessibilityLabel="1 new message"
            className="absolute -end-2 -top-2 size-6.5 items-center justify-center rounded-full border-[3px] border-background bg-red-500"
          >
            <Text className="text-sm font-semibold text-primary-foreground">
              1
            </Text>
          </View>
        </View>
        <Text
          role="heading"
          className="mt-6.5 text-center text-3xl font-semibold"
        >
          Check your inbox.
        </Text>
        <Text className="mt-3 text-center text-lg text-muted-foreground">
          We sent a verification link to{" "}
          <Text className="text-lg font-semibold text-foreground">{EMAIL}</Text>
          . Tap the link to activate your account.
        </Text>
        <View className="mt-6.5 flex-row flex-wrap justify-center gap-x-1">
          <Text className="text-sm text-muted-foreground">
            Didn't get it? Check your junk folder or
          </Text>
          <Pressable
            role="link"
            hitSlop={8}
            onPress={onChangeAddress}
            className="active:opacity-60"
          >
            <Text className="text-sm text-primary">change the address ›</Text>
          </Pressable>
        </View>
      </ScrollView>
      <BottomBar>
        <BottomBarContent>
          <Button
            size="lg"
            block
            onPress={() => {
              onOpenMail?.()
              setVerified(true)
            }}
          >
            <Text>Open Mail</Text>
          </Button>
          <Button
            size="lg"
            variant="outline"
            block
            disabled={left > 0}
            onPress={() => setLeft(45)}
          >
            <Text className="tabular-nums">
              {left > 0 ? `Sent again · ${left}` : "Resend"}
            </Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { VerifyEmail }
```

```swift title="swift/Blocks/VerifyEmail.swift"
import PhosphorSwift
import SwiftUI

public struct VerifyEmail: View {
  static let email = "jamie@example.com"

  private var onChangeAddress: (() -> Void)?
  private var onOpenMail: (() -> Void)?
  @State private var verified: Bool
  @State private var left: Int
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(onOpenMail: (() -> Void)? = nil, onChangeAddress: (() -> Void)? = nil) {
    self.init(verified: false, left: 0)
    self.onOpenMail = onOpenMail
    self.onChangeAddress = onChangeAddress
  }

  init(verified: Bool, left: Int) {
    _verified = State(initialValue: verified)
    _left = State(initialValue: left)
  }

  public var body: some View {
    Screen {
      if verified {
        done
          .transition(.opacity.combined(with: .offset(y: 18)))
      } else {
        KeyboardScrollView {
          inbox
        }
        BottomBar(separator: false) {
          BottomBarContent {
            Button("Open Mail") {
              onOpenMail?()
              verified = true
            }
              .buttonStyle(.variant(.default, size: .lg, block: true))
            Button {
              left = 45
            } label: {
              Text(left > 0 ? "Sent again · \(left)" : "Resend")
                .monospacedDigit()
            }
            .buttonStyle(.variant(.outline, size: .lg, block: true))
            .disabled(left > 0)
          }
        }
      }
    }
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.6), value: verified)
    .task(id: left) {
      guard left > 0 else { return }
      try? await Task.sleep(for: .seconds(1))
      guard !Task.isCancelled else { return }
      left -= 1
    }
  }

  private var inbox: some View {
    VStack(spacing: 0) {
      ZStack(alignment: .topTrailing) {
        Ph.envelopeSimple.regular
          .frame(width: 40, height: 40)
          .foregroundStyle(EsuissColor.primaryForeground)
          .frame(width: 96, height: 72)
          .background(
            EsuissColor.primary,
            in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
          )
          .shadow(Shadow(color: EsuissColor.primary.opacity(0.45), radius: 15, y: 10))
        Text(verbatim: "1")
          .scaledFont(.sm, weight: .semibold)
          .foregroundStyle(EsuissColor.primaryForeground)
          .frame(width: 26, height: 26)
          .background(Palette.red500, in: Circle())
          .padding(3)
          .background(EsuissColor.background, in: Circle())
          .offset(x: 11, y: -11)
      }
      .accessibilityElement(children: .ignore)
      .accessibilityLabel(Text("1 new message"))
      Text("Check your inbox.")
        .scaledTextStyle(.xl2, weight: .bold)
        .padding(.top, 26)
      Text("We sent a verification link to \(Text(verbatim: Self.email).fontWeight(.semibold).foregroundStyle(EsuissColor.foreground)). Tap the link to activate your account.")
        .scaledTextStyle(.lg)
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 12)
      VStack(spacing: 0) {
        Text("Didn't get it? Check your junk folder or")
          .scaledFont(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
        Button("change the address ›") { onChangeAddress?() }
          .buttonStyle(VerifyEmailLinkStyle())
      }
      .padding(.top, 26)
    }
    .multilineTextAlignment(.center)
    .fixedSize(horizontal: false, vertical: true)
    .frame(maxWidth: .infinity)
    .padding(.horizontal, 24)
    .padding(.top, 96)
    .padding(.bottom, 24)
  }

  private var done: some View {
    VStack(spacing: 14) {
      Ph.check.bold
        .frame(width: 38, height: 38)
        .foregroundStyle(EsuissColor.primaryForeground)
        .frame(width: 76, height: 76)
        .background(Palette.green500, in: Circle())
        .accessibilityHidden(true)
      Text("Your email is verified.")
        .scaledTextStyle(.xl2, weight: .bold)
      Text("\(Self.email) is now your suiss Account.")
        .scaledTextStyle(.lg)
        .foregroundStyle(EsuissColor.mutedForeground)
      Button("Start over ›") { verified = false }
        .buttonStyle(VerifyEmailLinkStyle(style: .lg))
    }
    .multilineTextAlignment(.center)
    .fixedSize(horizontal: false, vertical: true)
    .padding(24)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

private struct VerifyEmailLinkStyle: ButtonStyle {
  var style: TextStyle = .sm

  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .scaledFont(style)
      .foregroundStyle(EsuissColor.primary)
      .opacity(configuration.isPressed ? 0.8 : 1)
      .frame(minHeight: 32)
      .contentShape(Rectangle())
  }
}
```

```kt title="kotlin/blocks/VerifyEmail.kt"
package com.esuiss.ui.blocks

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.bold.Check
import com.adamglin.phosphoricons.regular.EnvelopeSimple
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.BottomBarContent
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Screen
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Shadow
import com.esuiss.ui.theme.Typography
import com.esuiss.ui.theme.shadow
import kotlinx.coroutines.delay

private const val DemoEmail = "jamie@example.com"

private val RevealEasing = CubicBezierEasing(0.32f, 0.72f, 0f, 1f)

@Composable
fun VerifyEmail(
    modifier: Modifier = Modifier,
    onOpenMail: () -> Unit = {},
    onChangeAddress: () -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var verified by rememberSaveable { mutableStateOf(false) }
    var left by rememberSaveable { mutableIntStateOf(0) }

    LaunchedEffect(left) {
        if (left <= 0) return@LaunchedEffect
        delay(1000)
        left -= 1
    }

    Screen(modifier) {
        if (verified) {
            Success(DemoEmail) { verified = false }
            return@Screen
        }
        Column(Modifier.fillMaxSize()) {
            BoxWithConstraints(Modifier.weight(1f)) {
                val height = maxHeight
                Column(
                    Modifier
                        .fillMaxSize()
                        .verticalScroll(rememberScrollState()),
                ) {
                    Column(
                        modifier = Modifier
                            .fillMaxWidth()
                            .heightIn(min = height)
                            .padding(24.dp),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {
                        MailGlyph()
                        Spacer(Modifier.height(26.dp))
                        BasicText(
                            "Check your inbox.",
                            modifier = Modifier.semantics { heading() },
                            style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
                        )
                        Spacer(Modifier.height(12.dp))
                        BasicText(
                            buildAnnotatedString {
                                append("We sent a verification link to ")
                                withStyle(SpanStyle(color = colors.foreground, fontWeight = FontWeight.SemiBold)) { append(DemoEmail) }
                                append(". Tap the link to activate your account.")
                            },
                            style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                        )
                        Spacer(Modifier.height(26.dp))
                        BasicText(
                            "Didn't get it? Check your junk folder or",
                            style = Typography.sm.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                        )
                        LinkText("change the address ›", onChangeAddress)
                    }
                }
            }
            BottomBar {
                BottomBarContent {
                    Button(
                        "Open Mail",
                        {
                            onOpenMail()
                            verified = true
                        },
                        size = ButtonSize.Lg,
                        block = true,
                    )
                    Button(
                        if (left > 0) "Sent again · $left" else "Resend",
                        {
                            left = 45
                        },
                        variant = ButtonVariant.Outline,
                        size = ButtonSize.Lg,
                        block = true,
                        enabled = left <= 0,
                    )
                }
            }
        }
    }
}

@Composable
private fun MailGlyph() {
    val colors = EsuissTheme.colors
    val shape = RoundedCornerShape(14.dp)
    Box(
        Modifier
            .size(width = 96.dp, height = 72.dp)
            .semantics(mergeDescendants = true) { contentDescription = "1 new message" },
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .shadow(Shadow(0.dp, 10.dp, 30.dp, colors.primary.copy(alpha = 0.45f)), shape)
                .background(colors.primary, shape),
            contentAlignment = Alignment.Center,
        ) {
            Icon(PhosphorIcons.Regular.EnvelopeSimple, contentDescription = null, size = 40.dp, tint = colors.primaryForeground)
        }
        Box(
            Modifier
                .align(Alignment.TopEnd)
                .offset(x = 8.dp, y = (-8).dp)
                .size(26.dp)
                .border(3.dp, colors.background, CircleShape)
                .padding(1.dp)
                .background(Palette.red500, CircleShape)
                .clearAndSetSemantics {},
            contentAlignment = Alignment.Center,
        ) {
            BasicText("1", style = Typography.sm.copy(color = colors.primaryForeground, fontWeight = FontWeight.SemiBold))
        }
    }
}

@Composable
private fun LinkText(text: String, onClick: () -> Unit) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    BasicText(
        text,
        modifier = Modifier
            .graphicsLayer { alpha = if (pressed) 0.8f else 1f }
            .clickable(interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick)
            .padding(vertical = 4.dp),
        style = Typography.sm.copy(color = EsuissTheme.colors.primary, textAlign = TextAlign.Center),
    )
}

@Composable
private fun Success(email: String, onStartOver: () -> Unit) {
    val colors = EsuissTheme.colors
    val appear = remember { Animatable(0f) }
    LaunchedEffect(Unit) { appear.animateTo(1f, tween(1000, easing = RevealEasing)) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
            .graphicsLayer {
                alpha = appear.value
                translationY = (1f - appear.value) * 18.dp.toPx()
            },
        verticalArrangement = Arrangement.spacedBy(14.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            Modifier
                .size(76.dp)
                .background(Palette.green500, CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Icon(PhosphorIcons.Bold.Check, contentDescription = null, size = 38.dp, tint = colors.primaryForeground)
        }
        BasicText(
            "Your email is verified.",
            modifier = Modifier.semantics { heading() },
            style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
        )
        BasicText(
            "$email is now your suiss Account.",
            style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
        )
        LinkText("Start over ›", onStartOver)
    }
}
```

The screen shows the address the link was sent to and a link to change it. The `BottomBar` has an Open Mail `Button` and a Resend `Button` that disables itself for a 45-second countdown. Opening mail shows a verified confirmation in this sample.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/verify-email
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add verify-email
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add verify-email --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add verify-email --platform kotlin
```

The CLI also adds what it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text). On React Native it also adds the animated view `components/ui/animated-view.tsx`.

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `uniwind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/uniwind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/verify-email.tsx`:

- `uniwind/components/blocks/verify-email.tsx`: shown above

5. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `nativewind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/nativewind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/verify-email.tsx`:

```tsx title="nativewind/components/blocks/verify-email.tsx"
import { CheckIcon, EnvelopeSimpleIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, ScrollView, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

const EMAIL = "jamie@example.com"

function VerifyEmail({
  onChangeAddress,
  onOpenMail,
}: {
  onChangeAddress?: () => void
  onOpenMail?: () => void
}) {
  const [verified, setVerified] = React.useState(false)
  const [left, setLeft] = React.useState(0)

  React.useEffect(() => {
    if (left <= 0) return
    const timer = setTimeout(() => setLeft(left - 1), 1000)
    return () => clearTimeout(timer)
  }, [left])

  if (verified) {
    return (
      <Screen>
        <View className="flex-1 items-center justify-center p-6">
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            className="items-center gap-3.5"
          >
            <View className="size-[4.75rem] items-center justify-center rounded-full bg-green-500">
              <Icon
                as={CheckIcon}
                weight="bold"
                className="size-[2.375rem] text-primary-foreground"
              />
            </View>
            <Text role="heading" className="text-center text-3xl font-semibold">
              Your email is verified.
            </Text>
            <Text className="text-center text-lg text-muted-foreground">
              {EMAIL} is now your suiss Account.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => setVerified(false)}
              className="active:opacity-60"
            >
              <Text className="text-lg text-primary">Start over ›</Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <ScrollView contentContainerClassName="grow items-center justify-center px-6 py-8">
        <View className="h-[4.5rem] w-24">
          <View className="absolute inset-0 items-center justify-center rounded-[0.875rem] bg-primary shadow-lg">
            <Icon
              as={EnvelopeSimpleIcon}
              className="size-10 text-primary-foreground"
            />
          </View>
          <View
            accessible
            accessibilityLabel="1 new message"
            className="absolute -end-2 -top-2 size-[1.625rem] items-center justify-center rounded-full border-[3px] border-background bg-red-500"
          >
            <Text className="text-sm font-semibold text-primary-foreground">
              1
            </Text>
          </View>
        </View>
        <Text
          role="heading"
          className="mt-[1.625rem] text-center text-3xl font-semibold"
        >
          Check your inbox.
        </Text>
        <Text className="mt-3 text-center text-lg text-muted-foreground">
          We sent a verification link to{" "}
          <Text className="text-lg font-semibold text-foreground">{EMAIL}</Text>
          . Tap the link to activate your account.
        </Text>
        <View className="mt-[1.625rem] flex-row flex-wrap justify-center gap-x-1">
          <Text className="text-sm text-muted-foreground">
            Didn't get it? Check your junk folder or
          </Text>
          <Pressable
            role="link"
            hitSlop={8}
            onPress={onChangeAddress}
            className="active:opacity-60"
          >
            <Text className="text-sm text-primary">change the address ›</Text>
          </Pressable>
        </View>
      </ScrollView>
      <BottomBar>
        <BottomBarContent>
          <Button
            size="lg"
            block
            onPress={() => {
              onOpenMail?.()
              setVerified(true)
            }}
          >
            <Text>Open Mail</Text>
          </Button>
          <Button
            size="lg"
            variant="outline"
            block
            disabled={left > 0}
            onPress={() => setLeft(45)}
          >
            <Text className="tabular-nums">
              {left > 0 ? `Sent again · ${left}` : "Resend"}
            </Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { VerifyEmail }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `stylesheet/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/stylesheet/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/verify-email.tsx`:

```tsx title="stylesheet/components/blocks/verify-email.tsx"
import { CheckIcon, EnvelopeSimpleIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, ScrollView, StyleSheet, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  radius,
  shadow,
  useTheme,
} from "@/components/ui/theme"

const EMAIL = "jamie@example.com"

const styles = StyleSheet.create({
  center: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    padding: 24,
  },
  stack: { alignItems: "center", gap: 14 },
  check: {
    width: 76,
    height: 76,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
  },
  title: { ...fontSize["3xl"], fontWeight: "600", textAlign: "center" },
  lead: { ...fontSize.lg, textAlign: "center" },
  content: {
    flexGrow: 1,
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 24,
    paddingVertical: 32,
  },
  envelope: { height: 72, width: 96 },
  tile: {
    position: "absolute",
    top: 0,
    right: 0,
    bottom: 0,
    left: 0,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 14,
  },
  badge: {
    position: "absolute",
    end: -8,
    top: -8,
    width: 26,
    height: 26,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
    borderWidth: 3,
  },
  footer: {
    marginTop: 26,
    flexDirection: "row",
    flexWrap: "wrap",
    justifyContent: "center",
    columnGap: 4,
  },
})

function VerifyEmail({
  onChangeAddress,
  onOpenMail,
}: {
  onChangeAddress?: () => void
  onOpenMail?: () => void
}) {
  const { colors } = useTheme()
  const [verified, setVerified] = React.useState(false)
  const [left, setLeft] = React.useState(0)

  React.useEffect(() => {
    if (left <= 0) return
    const timer = setTimeout(() => setLeft(left - 1), 1000)
    return () => clearTimeout(timer)
  }, [left])

  if (verified) {
    return (
      <Screen>
        <View style={styles.center}>
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            style={styles.stack}
          >
            <View style={[styles.check, { backgroundColor: palette.green500 }]}>
              <Icon
                as={CheckIcon}
                weight="bold"
                size={38}
                color={colors.primaryForeground}
              />
            </View>
            <Text role="heading" style={styles.title}>
              Your email is verified.
            </Text>
            <Text style={[styles.lead, { color: colors.mutedForeground }]}>
              {EMAIL} is now your suiss Account.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => setVerified(false)}
              style={({ pressed }) => pressed && { opacity: 0.6 }}
            >
              <Text style={[fontSize.lg, { color: colors.primary }]}>
                Start over ›
              </Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <ScrollView contentContainerStyle={styles.content}>
        <View style={styles.envelope}>
          <View
            style={[
              styles.tile,
              shadow.lg,
              { backgroundColor: colors.primary },
            ]}
          >
            <Icon
              as={EnvelopeSimpleIcon}
              size={40}
              color={colors.primaryForeground}
            />
          </View>
          <View
            accessible
            accessibilityLabel="1 new message"
            style={[
              styles.badge,
              {
                borderColor: colors.background,
                backgroundColor: palette.red500,
              },
            ]}
          >
            <Text
              style={[
                fontSize.sm,
                { fontWeight: "600", color: colors.primaryForeground },
              ]}
            >
              1
            </Text>
          </View>
        </View>
        <Text role="heading" style={[styles.title, { marginTop: 26 }]}>
          Check your inbox.
        </Text>
        <Text
          style={[
            styles.lead,
            { marginTop: 12, color: colors.mutedForeground },
          ]}
        >
          We sent a verification link to{" "}
          <Text
            style={[
              fontSize.lg,
              { fontWeight: "600", color: colors.foreground },
            ]}
          >
            {EMAIL}
          </Text>
          . Tap the link to activate your account.
        </Text>
        <View style={styles.footer}>
          <Text style={[fontSize.sm, { color: colors.mutedForeground }]}>
            Didn't get it? Check your junk folder or
          </Text>
          <Pressable
            role="link"
            hitSlop={8}
            onPress={onChangeAddress}
            style={({ pressed }) => pressed && { opacity: 0.6 }}
          >
            <Text style={[fontSize.sm, { color: colors.primary }]}>
              change the address ›
            </Text>
          </Pressable>
        </View>
      </ScrollView>
      <BottomBar>
        <BottomBarContent>
          <Button
            size="lg"
            block
            onPress={() => {
              onOpenMail?.()
              setVerified(true)
            }}
          >
            <Text>Open Mail</Text>
          </Button>
          <Button
            size="lg"
            variant="outline"
            block
            disabled={left > 0}
            onPress={() => setLeft(45)}
          >
            <Text style={{ fontVariant: ["tabular-nums"] }}>
              {left > 0 ? `Sent again · ${left}` : "Resend"}
            </Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { VerifyEmail }
```

5. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/VerifyEmail.swift`: shown above

- `swift/Components/BottomBar.swift`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/VerifyEmail.kt`: shown above

- `kotlin/components/BottomBar.kt`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { VerifyEmail } from "@/components/blocks/verify-email"
```

```tsx
<VerifyEmail />
```

Replace `EMAIL` with the user's address. Use `onOpenMail` to open the mail app and `onChangeAddress` to go back to the email field.

