├── apple-sign-in
└── generate-apple-secret.ts
├── basic-form
└── form.tsx
├── expo-auth-flow
├── components
│ ├── Button.tsx
│ └── ThemedText.tsx
├── icons
│ ├── AppleIcon.tsx
│ ├── GoogleIcon.tsx
│ └── MailIcon.tsx
└── index.tsx
├── fadein-wrapper.tsx
├── floating-navbar
├── Button.tsx
├── Card.tsx
├── Iconify.tsx
└── Navbar.tsx
└── pulsing-button
├── Button.tsx
└── style.css
/apple-sign-in/generate-apple-secret.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Apple Client Secret Generator Script
3 | *
4 | * This script generates a client secret JWT required for Apple Sign In authentication.
5 | * The generated secret is valid for 6 months and needs to be regenerated before expiry.
6 | *
7 | * Required Environment Variables in .env:
8 | * - APPLE_TEAM_ID: Found in your Apple Developer account
9 | * - APPLE_KEY_ID: The Key ID from your private key in Apple Developer Console
10 | * - APPLE_CLIENT_ID: Your app's identifier (e.g., com.yourapp.id)
11 | * - APPLE_PRIVATE_KEY_PATH: Path to your .p8 private key file
12 | *
13 | * Important Notes:
14 | * - The private key (.p8) file must be kept secure and never committed to version control
15 | * - The generated secret is valid for 6 months
16 | * - This secret is required for the auth.ts configuration (Better Auth)
17 | *
18 | * To run this script:
19 | * 1. Run the script: npm run generate-apple-secret
20 | * 2. The generated secret will be printed to the console
21 | * 3. Copy the secret and paste it into your .env file
22 | * 4. Restart your server
23 | */
24 |
25 | import dotenv from "dotenv";
26 | import fs from "fs";
27 | import pkg from "jsonwebtoken";
28 | import path from "path";
29 | const { sign } = pkg;
30 |
31 | // Load environment variables from .env file
32 | dotenv.config();
33 |
34 | // Log environment variables for verification
35 | // These logs help debug missing or incorrect values
36 | console.log("Environment variables:");
37 | console.log("TEAM_ID:", process.env.APPLE_TEAM_ID);
38 | console.log("KEY_ID:", process.env.APPLE_KEY_ID);
39 | console.log("CLIENT_ID:", process.env.APPLE_CLIENT_ID);
40 | console.log("PRIVATE_KEY_PATH:", process.env.APPLE_PRIVATE_KEY_PATH);
41 |
42 | // Extract required configuration from environment
43 | const TEAM_ID = process.env.APPLE_TEAM_ID;
44 | const KEY_ID = process.env.APPLE_KEY_ID;
45 | const CLIENT_ID = process.env.APPLE_CLIENT_ID;
46 | const PRIVATE_KEY_PATH = process.env.APPLE_PRIVATE_KEY_PATH;
47 |
48 | // Validate all required environment variables
49 | // Exit early if any required variable is missing
50 | if (!TEAM_ID || !KEY_ID || !CLIENT_ID || !PRIVATE_KEY_PATH) {
51 | console.error("Missing required environment variables");
52 | console.error(
53 | "Required variables: APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_CLIENT_ID, APPLE_PRIVATE_KEY_PATH"
54 | );
55 | process.exit(1);
56 | }
57 |
58 | console.log("Attempting to read private key from:", PRIVATE_KEY_PATH);
59 |
60 | try {
61 | // Read the Apple private key (.p8 file)
62 | // This key should be downloaded from Apple Developer Console
63 | const privateKey = fs.readFileSync(path.resolve(PRIVATE_KEY_PATH));
64 |
65 | // Configure the JWT payload according to Apple's specifications
66 | // See: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
67 | const payload = {
68 | iss: TEAM_ID, // Team ID from Apple Developer account
69 | iat: Math.floor(Date.now() / 1000), // Current timestamp
70 | exp: Math.floor(Date.now() / 1000) + 15777000, // 6 months expiry
71 | aud: "https://appleid.apple.com", // Required audience
72 | sub: CLIENT_ID, // Your app's identifier
73 | };
74 |
75 | // Generate the client secret
76 | // Uses ES256 algorithm as required by Apple
77 | const clientSecret = sign(payload, privateKey, {
78 | algorithm: "ES256",
79 | keyid: KEY_ID,
80 | });
81 |
82 | // Output the generated secret
83 | // This value should be used in your auth.ts configuration
84 | console.log("\nApple Client Secret:");
85 | console.log(clientSecret);
86 | } catch (error: any) {
87 | console.error("Error reading private key:", error.message);
88 | process.exit(1);
89 | }
90 |
--------------------------------------------------------------------------------
/basic-form/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | export default function Form() {
6 | const [state, setState] = useState({
7 | firstName: "",
8 | lastName: "",
9 | email: "",
10 | message: "",
11 | });
12 |
13 | const handleSubmit = (e: React.FormEvent) => {
14 | e.preventDefault();
15 | console.log(state);
16 |
17 | alert("Form submitted");
18 | };
19 |
20 | return (
21 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/expo-auth-flow/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText as Text } from "@/components/ThemedText";
2 | import clsx from "clsx";
3 | import React, { ReactNode } from "react";
4 | import { ActivityIndicator, Pressable } from "react-native";
5 |
6 | type Props = {
7 | onPress: () => void;
8 | children: ReactNode;
9 | isDisabled?: boolean;
10 | icon?: ReactNode;
11 | iconPosition?: "left" | "right";
12 | variant?: "primary" | "secondary";
13 | loading?: boolean;
14 | };
15 |
16 | export default function Button({
17 | onPress,
18 | children,
19 | isDisabled,
20 | icon,
21 | iconPosition = "left",
22 | variant = "primary",
23 | loading = false,
24 | }: Props) {
25 | return (
26 |
37 | {loading ? (
38 |
42 | ) : (
43 | <>
44 | {icon && iconPosition === "left" && icon}
45 |
54 | {children}
55 |
56 | {icon && iconPosition === "right" && icon}
57 | >
58 | )}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/expo-auth-flow/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, type TextProps } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = "default",
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: "600",
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: "bold",
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: "bold",
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: "#0a7ea4",
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/expo-auth-flow/icons/AppleIcon.tsx:
--------------------------------------------------------------------------------
1 | import Svg, { Path } from "react-native-svg";
2 |
3 | export function AppleIcon() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/expo-auth-flow/icons/GoogleIcon.tsx:
--------------------------------------------------------------------------------
1 | import Svg, { Path } from "react-native-svg";
2 |
3 | export function GoogleIcon() {
4 | return (
5 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/expo-auth-flow/icons/MailIcon.tsx:
--------------------------------------------------------------------------------
1 | import Svg, { Path } from "react-native-svg";
2 |
3 | export function MailIcon() {
4 | return (
5 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/expo-auth-flow/index.tsx:
--------------------------------------------------------------------------------
1 | import { AppleIcon, GoogleIcon, MailIcon } from "@/components/icons";
2 | import Button from "@/components/ui/Button";
3 | import { LinearGradient } from "expo-linear-gradient";
4 | import { Link, router } from "expo-router";
5 | import React, { useState } from "react";
6 | import { Dimensions, Image, Text, TextInput, View } from "react-native";
7 |
8 | export default function AuthScreen() {
9 | const screenHeight = Dimensions.get("window").height;
10 |
11 | const [showEmailInputs, setShowEmailInputs] = useState(false);
12 | const [email, setEmail] = useState("");
13 | const [password, setPassword] = useState("");
14 |
15 | return (
16 |
17 |
28 |
29 |
39 |
40 |
41 |
42 | Login
43 |
44 |
45 | {!showEmailInputs ? (
46 |
47 | }
49 | onPress={() => setShowEmailInputs(true)}
50 | >
51 | Continue with Email
52 |
53 |
54 | }
56 | onPress={() => router.push("/signin")}
57 | >
58 | Continue With Google
59 |
60 |
61 | }
63 | onPress={() => router.push("/signin")}
64 | >
65 | Continue With Apple
66 |
67 |
68 |
69 | Dont have an account?{" "}
70 |
74 | Create Account
75 |
76 |
77 |
78 | ) : (
79 |
80 |
89 |
90 |
97 |
98 |
101 |
102 |
108 |
109 | )}
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/fadein-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import clsx from "clsx";
4 | import { useEffect, useRef, useState } from "react";
5 |
6 | interface FadeInProps {
7 | children: React.ReactNode;
8 | duration?: number; // delay in milliseconds
9 | className?: string;
10 | }
11 |
12 | export default function FadeIn({
13 | children,
14 | duration = 0,
15 | className,
16 | }: FadeInProps) {
17 | const [isVisible, setIsVisible] = useState(false);
18 | const elementRef = useRef(null);
19 |
20 | useEffect(() => {
21 | const observer = new IntersectionObserver(
22 | ([entry]) => {
23 | if (entry.isIntersecting) {
24 | setTimeout(() => {
25 | setIsVisible(true);
26 | }, duration);
27 | }
28 | },
29 | {
30 | threshold: 0.1, // Trigger when 10% of element is visible
31 | }
32 | );
33 |
34 | if (elementRef.current) {
35 | observer.observe(elementRef.current);
36 | }
37 |
38 | return () => {
39 | if (elementRef.current) {
40 | observer.unobserve(elementRef.current);
41 | }
42 | };
43 | }, [duration]);
44 |
45 | return (
46 |
56 | {children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/floating-navbar/Button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import clsx from "clsx";
4 | import { ButtonHTMLAttributes, ReactNode } from "react";
5 |
6 | // Define variant types
7 | type ButtonVariant = "ghost";
8 |
9 | // Add icon position type
10 | type IconPosition = "left" | "right";
11 |
12 | // Add size type
13 | type ButtonSize = "small" | "large";
14 |
15 | type ButtonProps = {
16 | children: string | ReactNode;
17 | variant?: ButtonVariant;
18 | size?: ButtonSize;
19 | icon?: ReactNode;
20 | iconPosition?: IconPosition;
21 | isLoading?: boolean;
22 | loaderColor?: string;
23 | isDisabled?: boolean;
24 | width?: string;
25 | padding?: string;
26 | textSize?: string;
27 | } & ButtonHTMLAttributes;
28 |
29 | // Base button classes
30 | const baseButtonClasses =
31 | "h-fit rounded-xl font-normal text-center flex justify-center transition-all duration-300 hover:scale-105 hover:shadow-lg select-none";
32 |
33 | // Variant classes mapping
34 | const variantClasses: Record = {
35 | ghost: "bg-transparent text-white",
36 | };
37 |
38 | // Add size classes mapping
39 | const sizeClasses: Record = {
40 | small: "py-2 px-4 text-[12px]",
41 | large: "py-3 px-6 text-[16px]",
42 | };
43 |
44 | export default function Button({
45 | children,
46 | variant = "primary",
47 | size = "large",
48 | icon,
49 | iconPosition = "left",
50 | isLoading = false,
51 | loaderColor = "text-current",
52 | isDisabled = false,
53 | width = "w-fit",
54 | padding,
55 | textSize,
56 | className,
57 | ...props
58 | }: ButtonProps) {
59 | const content = (
60 | <>
61 | {isLoading ? (
62 |
68 | ) : (
69 |
70 | {icon && iconPosition === "left" && icon}
71 | {children}
72 | {icon && iconPosition === "right" && icon}
73 |
74 | )}
75 | >
76 | );
77 |
78 | return (
79 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/floating-navbar/Card.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type CardProps = {
4 | width?: string;
5 | cursor?: string;
6 | bgColor?: string;
7 | padding?: string;
8 | children: React.ReactNode;
9 | border?: string;
10 | borderColor?: string;
11 | className?: string;
12 | onClick?: (e: React.MouseEvent) => void;
13 | };
14 |
15 | export default function Card({
16 | width = "w-full",
17 | cursor = "default",
18 | bgColor = "bg-[linear-gradient(118deg,_#111_3.48%,_rgba(17,_17,_17,_0.00)_100%)]",
19 | padding = "p-4",
20 | children,
21 | onClick,
22 | border = "border-x border-t",
23 | borderColor = "border-[rgb(48,48,48)]",
24 | className,
25 | }: CardProps) {
26 | return (
27 |
40 | {children}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/floating-navbar/Iconify.tsx:
--------------------------------------------------------------------------------
1 | import { Icon, IconifyIcon } from "@iconify/react";
2 | import { BaseHTMLAttributes } from "react";
3 |
4 | export interface IconifyProps extends BaseHTMLAttributes {
5 | icon: IconifyIcon | string;
6 | }
7 |
8 | export function Iconify({ icon, ...other }: IconifyProps) {
9 | return (
10 |
11 | {/* Find icons here -> https://icon-sets.iconify.design/ */}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/floating-navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NAV_OPTIONS } from "@/data/navigation";
4 | import clsx from "clsx";
5 | import { useState } from "react";
6 | import Logo from "./branding/Logo";
7 | import Button from "./ui/Button";
8 | import Card from "./ui/Card";
9 | import { Iconify } from "./ui/Iconify";
10 |
11 | export default function Navbar() {
12 | const [isMenuOpen, setIsMenuOpen] = useState(false);
13 |
14 | const handleScroll = (e: React.MouseEvent, id: string) => {
15 | e.preventDefault();
16 | const element = document.getElementById(id);
17 | element?.scrollIntoView({ behavior: "smooth" });
18 | setIsMenuOpen(false);
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {NAV_OPTIONS.map((option) => (
30 |
37 | ))}
38 |
39 |
40 |
44 |
45 |
46 |
52 |
53 |
54 | {NAV_OPTIONS.map((option) => (
55 |
64 | ))}
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | const MenuButton = ({
75 | isMenuOpen,
76 | setIsMenuOpen,
77 | }: {
78 | isMenuOpen: boolean;
79 | setIsMenuOpen: (isMenuOpen: boolean) => void;
80 | }) => {
81 | return (
82 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/pulsing-button/Button.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 | import { ButtonHTMLAttributes, ReactNode } from "react";
3 |
4 | // Define variant types
5 | type ButtonVariant = "primary";
6 |
7 | // Add icon position type
8 | type IconPosition = "left" | "right";
9 |
10 | type ButtonProps = {
11 | children: string | ReactNode;
12 | variant?: ButtonVariant;
13 | icon?: ReactNode;
14 | iconPosition?: IconPosition;
15 | isLoading?: boolean;
16 | loaderColor?: string;
17 | isDisabled?: boolean;
18 | width?: string;
19 | padding?: string;
20 | textSize?: string;
21 | requiresSubscription?: boolean;
22 | isSubscribed?: boolean;
23 | } & ButtonHTMLAttributes;
24 |
25 | // Base button classes
26 | const baseButtonClasses =
27 | "h-fit py-2.5 px-4 text-[14px] font-bold min-w-[100px] text-center flex justify-center transition-all duration-300";
28 |
29 | // Variant classes mapping
30 | const variantClasses: Record = {
31 | primary:
32 | "bg-[rgb(124, 58, 237)] text-white group-[.not-disabled]:hover:opacity-90 rounded-full",
33 | };
34 |
35 | export default function Button({
36 | children,
37 | variant = "primary",
38 | icon,
39 | iconPosition = "left",
40 | isLoading = false,
41 | loaderColor = "text-current",
42 | isDisabled = false,
43 | width = "w-fit",
44 | padding,
45 | textSize,
46 | className,
47 | requiresSubscription = false,
48 | isSubscribed,
49 | ...props
50 | }: ButtonProps) {
51 | return (
52 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/pulsing-button/style.css:
--------------------------------------------------------------------------------
1 | .animation-pulse {
2 | position: relative;
3 | }
4 |
5 | .animation-pulse::before {
6 | content: "";
7 | position: absolute;
8 | inset: -3px;
9 | border-radius: inherit;
10 | animation: pulse 2s infinite;
11 | }
12 |
13 | @keyframes pulse {
14 | 0% {
15 | box-shadow: 0 0 0 0 rgb(124, 58, 237);
16 | }
17 |
18 | 70% {
19 | box-shadow: 0 0 0 10px rgba(229, 62, 62, 0);
20 | }
21 |
22 | 100% {
23 | box-shadow: 0 0 0 0 rgba(229, 62, 62, 0);
24 | }
25 | }
26 |
27 | @layer utilities {
28 | .perspective-1000 {
29 | perspective: 1000px;
30 | }
31 |
32 | .transform-style-3d {
33 | transform-style: preserve-3d;
34 | }
35 |
36 | .backface-hidden {
37 | backface-visibility: hidden;
38 | }
39 |
40 | .rotate-y-180 {
41 | transform: rotateY(180deg);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------