diff --git a/community/privy-auth/.env.example b/community/privy-auth/.env.example new file mode 100644 index 00000000..c47731ac --- /dev/null +++ b/community/privy-auth/.env.example @@ -0,0 +1,8 @@ +# Privy App ID — Get yours at https://dashboard.privy.io +NEXT_PUBLIC_PRIVY_APP_ID=your-privy-app-id + +# Privy App Secret — Get yours at https://dashboard.privy.io (Settings > API Keys) +PRIVY_APP_SECRET=your-privy-app-secret + +# Solana RPC URL (defaults to devnet if not set) +NEXT_PUBLIC_SOLANA_RPC_URL=https://api.devnet.solana.com diff --git a/community/privy-auth/.gitignore b/community/privy-auth/.gitignore new file mode 100644 index 00000000..a0530a8f --- /dev/null +++ b/community/privy-auth/.gitignore @@ -0,0 +1,33 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/community/privy-auth/.prettierrc b/community/privy-auth/.prettierrc new file mode 100644 index 00000000..7e7cba01 --- /dev/null +++ b/community/privy-auth/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/community/privy-auth/README.md b/community/privy-auth/README.md new file mode 100644 index 00000000..41891424 --- /dev/null +++ b/community/privy-auth/README.md @@ -0,0 +1,433 @@ +

+ Solana +      + Privy +

+ +

privy-auth

+ +

+ Solana dApp template with Privy authentication +

+ +

+ Social logins • Embedded wallets • Protected routes • Ready to ship +

+ +

+ Quick Start • + Features • + Setup • + Customize • + Resources +

+ +

+ Next.js + TypeScript + Tailwind + Solana + Privy + MIT +

+ +--- + +## Quick Start + +```bash +# Scaffold with create-solana-dapp (recommended) +pnpm create solana-dapp --template privy-auth + +# Or clone directly +npx degit solana-developers/solana-templates/community/privy-auth my-app +cd my-app && pnpm install +``` + +
+npm / yarn + +```bash +# npm +npx create-solana-dapp --template privy-auth + +# yarn +yarn create solana-dapp --template privy-auth +``` + +
+ +--- + +## Features + +``` ++-------------------+ +-------------------+ +-------------------+ +| Social Login | | Embedded Wallets | | Protected Routes | +| | | | | | +| Google | | Auto-created | | Middleware guard | +| Twitter/X | | Solana wallet | | Client-side | +| Discord | | for every user | | auth check | +| Email | | on login | | | ++-------------------+ +-------------------+ +-------------------+ + ++-------------------+ +-------------------+ +-------------------+ +| External Wallets | | Sign Messages | | Wallet State | +| | | | | | +| Phantom | | Demo component | | Privy hooks for | +| Solflare | | to sign with | | easy wallet | +| Backpack | | embedded wallet | | access | +| + more | | | | | ++-------------------+ +-------------------+ +-------------------+ +``` + +--- + +## Tech Stack + +| | Technology | Version | +|---|-----------|---------| +| **Framework** | Next.js (App Router) | ^15.3 | +| **Language** | TypeScript | ^5 | +| **Styling** | Tailwind CSS | ^4 | +| **Auth** | Privy React SDK | ^3.0 | +| **Blockchain** | Solana Kit | ^6.1 | + +--- + +## Setup Guide + +### 1. Create a Privy App + +> **Takes ~2 minutes.** Free tier is all you need. + +1. Go to **[dashboard.privy.io](https://dashboard.privy.io)** and sign up +2. Click **Create App** +3. Enable login methods in the sidebar: + + | Method | Toggle | + |--------|--------| + | Google | On | + | Twitter | On | + | Discord | On | + | Email | On | + | Wallet | On | + +4. Go to **Embedded Wallets** > ensure **Solana** is enabled +5. Go to **Settings > API Keys** > copy your **App ID** + +### 2. Environment Variables + +```bash +cp .env.example .env.local +``` + +```env +# Required +NEXT_PUBLIC_PRIVY_APP_ID=cmxxxxxxxxxxxxxxxxx + +# Optional — for server-side token verification +PRIVY_APP_SECRET=privy_app_secret_xxxxxxxxx + +# Optional — defaults to devnet +NEXT_PUBLIC_SOLANA_RPC_URL=https://api.devnet.solana.com +``` + +### 3. Run + +```bash +pnpm install +pnpm dev +``` + +Open **[localhost:3000](http://localhost:3000)** — you should see the landing page. + +> If you see "Missing Privy App ID", double-check your `.env.local` file. + +--- + +## Project Structure + +``` +privy-auth/ +│ +├── src/ +│ ├── app/ +│ │ ├── layout.tsx .............. Root layout with PrivyProvider +│ │ ├── page.tsx ............... Landing page + login button +│ │ ├── globals.css ............ Dark theme + Tailwind +│ │ └── dashboard/ +│ │ └── page.tsx ........... Protected dashboard +│ │ +│ ├── components/ +│ │ ├── providers.tsx .......... Privy config (logins, wallets, theme) +│ │ ├── login-button.tsx ....... Opens modal, redirects on success +│ │ ├── user-profile.tsx ....... User ID, wallets, linked accounts +│ │ ├── wallet-info.tsx ........ Status indicators (green/red dots) +│ │ └── sign-message.tsx ....... Sign demo with embedded wallet +│ │ +│ +├── middleware.ts ................... Route guard (privy-token cookie) +├── .env.example ................... Environment variable template +└── package.json ................... Deps + create-solana-dapp config +``` + +--- + +## How It Works + +### Auth Flow + +``` + ┌─────────────────────┐ + │ Landing Page │ + │ (page.tsx) │ + └──────────┬──────────┘ + │ + Click "Sign In with Privy" + │ + ┌──────────▼──────────┐ + │ Privy Modal │ + │ │ + │ ┌───────────────┐ │ + │ │ Google │ │ + │ │ Twitter/X │ │ + │ │ Discord │ │ + │ │ Email │ │ + │ │ Wallet │ │ + │ └───────────────┘ │ + └──────────┬──────────┘ + │ + User authenticates + │ + ┌──────────▼──────────┐ + │ Embedded Solana │ + │ wallet created │ + │ automatically │ + └──────────┬──────────┘ + │ + privy-token cookie set + │ + ┌──────────▼──────────┐ + │ /dashboard │ + │ │ + │ Profile + Wallets │ + │ Sign Message Demo │ + │ Logout │ + └─────────────────────┘ +``` + +### Route Protection + +Two layers keep `/dashboard` secure: + +| Layer | File | What it does | +|-------|------|-------------| +| **Middleware** | `middleware.ts` | No `privy-token` cookie? Redirect to `/` | +| **Client** | `dashboard/page.tsx` | `usePrivy()` not authenticated? Redirect to `/` | + +--- + +## Customization + +
+Change Login Methods + +Edit `src/components/providers.tsx`: + +```tsx +loginMethods: ["google", "twitter", "discord", "email", "wallet"], +``` + +Available: `"google"`, `"twitter"`, `"discord"`, `"email"`, `"wallet"`, `"apple"`, `"github"`, `"linkedin"`, `"tiktok"`, `"farcaster"`, `"phone"` + +> Also toggle matching methods in **[Privy Dashboard](https://dashboard.privy.io) > Login Methods**. + +
+ +
+Change Theme & Appearance + +```tsx +// src/components/providers.tsx +appearance: { + theme: "dark", // "dark" | "light" + accentColor: "#9945FF", // any hex color + showWalletLoginFirst: false, // wallet options first? + walletChainType: "solana-only", +}, +``` + +
+ +
+Change Embedded Wallet Behavior + +```tsx +// src/components/providers.tsx +embeddedWallets: { + solana: { + createOnLogin: "users-without-wallets", + // "all-users" — wallet for everyone + // "users-without-wallets" — only if no external wallet + // "off" — no auto-creation + }, +}, +``` + +
+ +
+Use the Wallet Hook + +```tsx +import { useWallets } from "@privy-io/react-auth/solana"; + +function MyComponent() { + const { wallets, ready } = useWallets(); + + // wallets — all connected Solana wallets + // ready — boolean (whether wallets are ready) + + const embedded = wallets.find((w) => w.standardWallet.name === "Privy") ?? null; + const external = wallets.filter((w) => w.standardWallet.name !== "Privy"); +} +``` + +
+ +
+Sign Messages + +```tsx +import { + useWallets, + useSignMessage, +} from "@privy-io/react-auth/solana"; + +function MyComponent() { + const { wallets } = useWallets(); + const { signMessage } = useSignMessage(); + const embedded = wallets.find((w) => w.standardWallet.name === "Privy"); + + const handleSign = async () => { + if (!embedded) return; + const msg = new TextEncoder().encode("Hello, Solana!"); + const { signature } = await signMessage({ + message: msg, + wallet: embedded, + }); + console.log("Signature:", Buffer.from(signature).toString("base64")); + }; +} +``` + +
+ +
+Add a New Protected Route + +1. Create `src/app/dashboard/settings/page.tsx` +2. Middleware already covers `/dashboard/*` — no config needed +3. Add the client guard: + +```tsx +"use client"; +import { usePrivy } from "@privy-io/react-auth"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +export default function SettingsPage() { + const { ready, authenticated } = usePrivy(); + const router = useRouter(); + + useEffect(() => { + if (ready && !authenticated) router.replace("/"); + }, [ready, authenticated, router]); + + if (!ready) return

Loading...

; + if (!authenticated) return null; + + return
Your protected content
; +} +``` + +
+ +
+Switch to Mainnet + +```env +NEXT_PUBLIC_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +``` + +For production, use [Helius](https://helius.dev), [QuickNode](https://quicknode.com), or [Triton](https://triton.one). + +
+ +--- + +## Scripts + +| Command | Description | +|---------|-------------| +| `pnpm dev` | Dev server at `localhost:3000` | +| `pnpm build` | Production build | +| `pnpm start` | Start production server | +| `pnpm lint` | Run ESLint | + +--- + +## Troubleshooting + +
+"Missing Privy App ID" screen + +Set `NEXT_PUBLIC_PRIVY_APP_ID` in `.env.local`. Get it from [dashboard.privy.io](https://dashboard.privy.io) > Settings > API Keys. + +
+ +
+Social login not working + +Enable the method in **both** places: +1. `providers.tsx` > `loginMethods` array +2. [Privy Dashboard](https://dashboard.privy.io) > Login Methods + +
+ +
+Embedded wallet not created + +Check `embeddedWallets.solana.createOnLogin` is `"users-without-wallets"` in `providers.tsx`. Also enable **Embedded Wallets > Solana** in the Privy Dashboard. + +
+ +
+Hydration errors in console + +Usually browser extensions (wallets, ad blockers) injecting DOM elements. Cosmetic only — doesn't affect functionality. + +
+ +--- + +## Resources + +| | Link | +|---|------| +| Privy Docs | [docs.privy.io](https://docs.privy.io) | +| Privy + Solana Guide | [Getting Started](https://docs.privy.io/recipes/solana/getting-started-with-privy-and-solana) | +| Privy Dashboard | [dashboard.privy.io](https://dashboard.privy.io) | +| Privy SDK Reference | [React Auth](https://docs.privy.io/reference/sdk/react-auth) | +| Solana Docs | [solana.com/docs](https://solana.com/docs) | +| create-solana-dapp | [GitHub](https://github.com/solana-developers/create-solana-dapp) | +| Solana Templates | [GitHub](https://github.com/solana-developers/solana-templates) | + +--- + +

+ Built with Privy + Solana +
+ MIT License +

diff --git a/community/privy-auth/components.json b/community/privy-auth/components.json new file mode 100644 index 00000000..fdcefc1f --- /dev/null +++ b/community/privy-auth/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/community/privy-auth/eslint.config.mjs b/community/privy-auth/eslint.config.mjs new file mode 100644 index 00000000..c6d02a96 --- /dev/null +++ b/community/privy-auth/eslint.config.mjs @@ -0,0 +1,12 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ baseDirectory: __dirname }); + +const eslintConfig = [...compat.extends("next/core-web-vitals")]; + +export default eslintConfig; diff --git a/community/privy-auth/middleware.ts b/community/privy-auth/middleware.ts new file mode 100644 index 00000000..02434369 --- /dev/null +++ b/community/privy-auth/middleware.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from "next/server"; + +export function middleware(req: NextRequest) { + const privyToken = req.cookies.get("privy-token"); + + if (!privyToken && req.nextUrl.pathname.startsWith("/dashboard")) { + return NextResponse.redirect(new URL("/", req.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*"], +}; diff --git a/community/privy-auth/next.config.ts b/community/privy-auth/next.config.ts new file mode 100644 index 00000000..cb651cdc --- /dev/null +++ b/community/privy-auth/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/community/privy-auth/package.json b/community/privy-auth/package.json new file mode 100644 index 00000000..c41d7720 --- /dev/null +++ b/community/privy-auth/package.json @@ -0,0 +1,80 @@ +{ + "name": "privy-auth", + "version": "0.0.0", + "private": true, + "displayName": "Privy Auth", + "description": "Next.js Solana starter with Privy authentication, social logins, and embedded wallets", + "usecase": "Auth", + "keywords": [ + "solana", + "nextjs", + "privy", + "auth", + "social-login", + "embedded-wallet", + "typescript", + "tailwind" + ], + "create-solana-dapp": { + "instructions": [ + "Set up your Privy App:", + "1. Go to https://dashboard.privy.io and create a new app", + "2. Enable desired login methods (Google, Twitter, Discord, Email)", + "3. Copy your App ID and App Secret", + "", + "Configure environment variables:", + "Copy .env.example to .env.local and fill in your values", + "", + "Install dependencies:", + "+{pm} install", + "", + "Start dev server:", + "+{pm} dev" + ], + "rename": { + "privy-auth": { + "to": "{{name}}", + "paths": [ + "README.md", + "src" + ] + } + } + }, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint ." + }, + "dependencies": { + "@privy-io/react-auth": "^3.0.0", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tooltip": "^1.2.8", + "@solana/kit": "^6.1.0", + "@solana-program/memo": "^0.8.0", + "@solana-program/system": "^0.12.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.575.0", + "next": "^15.3.0", + "next-themes": "^0.4.6", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.4", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "^15.3.0", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/community/privy-auth/postcss.config.mjs b/community/privy-auth/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/community/privy-auth/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/community/privy-auth/src/app/dashboard/page.tsx b/community/privy-auth/src/app/dashboard/page.tsx new file mode 100644 index 00000000..ead7bf7e --- /dev/null +++ b/community/privy-auth/src/app/dashboard/page.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { usePrivy } from "@privy-io/react-auth"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { Wallet } from "lucide-react"; +import { UserProfile } from "@/components/user-profile"; +import { WalletInfo } from "@/components/wallet-info"; +import { SignMessage } from "@/components/sign-message"; +import { ThemeToggle } from "@/components/theme-toggle"; + +export default function DashboardPage() { + const { ready, authenticated } = usePrivy(); + const router = useRouter(); + + useEffect(() => { + if (ready && !authenticated) { + router.replace("/"); + } + }, [ready, authenticated, router]); + + if (!ready) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (!authenticated) return null; + + return ( +
+ {/* Dot grid background */} +
+ + {/* Nav */} + + + {/* Content */} +
+ {/* Header */} +
+

Dashboard

+

+ Manage your wallet and account +

+
+ + {/* Cards */} +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ ); +} diff --git a/community/privy-auth/src/app/globals.css b/community/privy-auth/src/app/globals.css new file mode 100644 index 00000000..77f1803a --- /dev/null +++ b/community/privy-auth/src/app/globals.css @@ -0,0 +1,248 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.75rem; + --background: oklch(0.985 0.002 90); + --foreground: oklch(0.145 0.015 260); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0.015 260); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0.015 260); + --primary: oklch(0.48 0.22 277); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.96 0.005 260); + --secondary-foreground: oklch(0.24 0.015 260); + --muted: oklch(0.955 0.005 260); + --muted-foreground: oklch(0.50 0.01 260); + --accent: oklch(0.955 0.005 260); + --accent-foreground: oklch(0.24 0.015 260); + --destructive: oklch(0.60 0.21 25); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.91 0.005 260); + --input: oklch(0.91 0.005 260); + --ring: oklch(0.48 0.22 277); + --chart-1: oklch(0.48 0.22 277); + --chart-2: oklch(0.68 0.13 182); + --chart-3: oklch(0.77 0.16 70); + --chart-4: oklch(0.65 0.21 354); + --chart-5: oklch(0.72 0.19 150); + --sidebar: oklch(0.985 0.002 90); + --sidebar-foreground: oklch(0.145 0.015 260); + --sidebar-primary: oklch(0.48 0.22 277); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.955 0.005 260); + --sidebar-accent-foreground: oklch(0.24 0.015 260); + --sidebar-border: oklch(0.91 0.005 260); + --sidebar-ring: oklch(0.48 0.22 277); + --font-sans: DM Sans, sans-serif; + --font-serif: DM Sans, sans-serif; + --font-mono: Space Mono, monospace; + --shadow-color: #1a1a1a; + --shadow-opacity: 0.05; + --shadow-blur: 0px; + --shadow-spread: 0px; + --shadow-offset-x: 0px; + --shadow-offset-y: 0px; + --letter-spacing: normal; + --spacing: 0.25rem; + --shadow-2xs: 0 1px 2px 0 rgb(0 0 0 / 0.02); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.03); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.06); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.06), 0 2px 4px -2px rgb(0 0 0 / 0.06); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.06), 0 4px 6px -4px rgb(0 0 0 / 0.06); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.06), 0 8px 10px -6px rgb(0 0 0 / 0.06); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.15); + --tracking-normal: normal; +} + +html { + color-scheme: light; +} + +html.dark { + color-scheme: dark; +} + +body { + @apply bg-background text-foreground antialiased; +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --font-sans: DM Sans, sans-serif; + --font-mono: Space Mono, monospace; + --font-serif: DM Sans, sans-serif; + --radius: 0.75rem; + --tracking-tighter: calc(var(--tracking-normal) - 0.05em); + --tracking-tight: calc(var(--tracking-normal) - 0.025em); + --tracking-wide: calc(var(--tracking-normal) + 0.025em); + --tracking-wider: calc(var(--tracking-normal) + 0.05em); + --tracking-widest: calc(var(--tracking-normal) + 0.1em); + --tracking-normal: var(--tracking-normal); + --shadow-2xl: var(--shadow-2xl); + --shadow-xl: var(--shadow-xl); + --shadow-lg: var(--shadow-lg); + --shadow-md: var(--shadow-md); + --shadow: var(--shadow); + --shadow-sm: var(--shadow-sm); + --shadow-xs: var(--shadow-xs); + --shadow-2xs: var(--shadow-2xs); + --spacing: var(--spacing); + --letter-spacing: var(--letter-spacing); + --shadow-offset-y: var(--shadow-offset-y); + --shadow-offset-x: var(--shadow-offset-x); + --shadow-spread: var(--shadow-spread); + --shadow-blur: var(--shadow-blur); + --shadow-opacity: var(--shadow-opacity); + --color-shadow-color: var(--shadow-color); + --color-destructive-foreground: var(--destructive-foreground); +} + +.dark { + --background: oklch(0.13 0.015 260); + --foreground: oklch(0.97 0.005 260); + --card: oklch(0.18 0.015 260); + --card-foreground: oklch(0.97 0.005 260); + --popover: oklch(0.18 0.015 260); + --popover-foreground: oklch(0.97 0.005 260); + --primary: oklch(0.68 0.16 277); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.22 0.015 260); + --secondary-foreground: oklch(0.90 0.005 260); + --muted: oklch(0.22 0.015 260); + --muted-foreground: oklch(0.65 0.01 260); + --accent: oklch(0.22 0.015 260); + --accent-foreground: oklch(0.90 0.005 260); + --destructive: oklch(0.65 0.20 25); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.28 0.015 260); + --input: oklch(0.28 0.015 260); + --ring: oklch(0.68 0.16 277); + --chart-1: oklch(0.68 0.16 277); + --chart-2: oklch(0.78 0.13 182); + --chart-3: oklch(0.88 0.15 92); + --chart-4: oklch(0.73 0.18 350); + --chart-5: oklch(0.80 0.18 152); + --sidebar: oklch(0.13 0.015 260); + --sidebar-foreground: oklch(0.97 0.005 260); + --sidebar-primary: oklch(0.68 0.16 277); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.22 0.015 260); + --sidebar-accent-foreground: oklch(0.90 0.005 260); + --sidebar-border: oklch(0.28 0.015 260); + --sidebar-ring: oklch(0.68 0.16 277); +} + +@layer base { + body { + letter-spacing: var(--tracking-normal); + } +} + +/* Dot grid background pattern */ +.dot-grid { + background-image: radial-gradient(circle, oklch(0.75 0.01 260) 1px, transparent 1px); + background-size: 24px 24px; +} + +.dark .dot-grid { + background-image: radial-gradient(circle, oklch(0.30 0.01 260) 1px, transparent 1px); +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} + +.animate-fade-up { + animation: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.animate-fade-in { + animation: fade-in 0.5s ease-out both; +} + +.animate-scale-in { + animation: scale-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} diff --git a/community/privy-auth/src/app/layout.tsx b/community/privy-auth/src/app/layout.tsx new file mode 100644 index 00000000..f3652077 --- /dev/null +++ b/community/privy-auth/src/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from "next"; +import { DM_Sans, Space_Mono } from "next/font/google"; +import { ThemeProvider } from "next-themes"; +import { Providers } from "@/components/providers"; +import { Toaster } from "@/components/ui/sonner"; +import "./globals.css"; + +const dmSans = DM_Sans({ + subsets: ["latin"], + variable: "--font-sans", +}); + +const spaceMono = Space_Mono({ + weight: ["400", "700"], + subsets: ["latin"], + variable: "--font-mono", +}); + +export const metadata: Metadata = { + title: "privy-auth — Solana Privy Auth Template", + description: + "Solana dApp starter with Privy authentication, social logins, and embedded wallets", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + + ); +} diff --git a/community/privy-auth/src/app/page.tsx b/community/privy-auth/src/app/page.tsx new file mode 100644 index 00000000..3814ee8d --- /dev/null +++ b/community/privy-auth/src/app/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { usePrivy } from "@privy-io/react-auth"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { LoginButton } from "@/components/login-button"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { + Wallet, + ShieldCheck, + Zap, + ArrowRight, + Chrome, + MessageCircle, +} from "lucide-react"; + +const features = [ + { + icon: Chrome, + title: "Google", + desc: "One-tap sign in", + }, + { + icon: MessageCircle, + title: "Discord", + desc: "Community login", + }, + { + icon: Zap, + title: "Twitter", + desc: "Social auth", + }, + { + icon: Wallet, + title: "Wallet", + desc: "Phantom, Solflare & more", + }, +]; + +const highlights = [ + { + icon: Wallet, + title: "Embedded Wallets", + desc: "Auto-created Solana wallet for users without one", + }, + { + icon: ShieldCheck, + title: "Protected Routes", + desc: "Middleware and client-side auth guards", + }, + { + icon: Zap, + title: "Instant Setup", + desc: "Drop-in Privy provider, ready to build", + }, +]; + +export default function Home() { + const { ready, authenticated } = usePrivy(); + const router = useRouter(); + + useEffect(() => { + if (ready && authenticated) { + router.replace("/dashboard"); + } + }, [ready, authenticated, router]); + + return ( +
+ {/* Dot grid background */} +
+ + {/* Top nav */} + + + {/* Hero */} +
+ {/* Gradient orb */} +
+
+
+ +
+ {/* Badge */} +
+
+ + Solana + Privy Auth Template +
+
+ + {/* Headline */} +
+

+ Auth that just{" "} + works +

+

+ Social logins, embedded wallets, and protected routes for your + Solana dApp. Built with Privy. +

+
+ + {/* Login methods grid */} +
+
+ {features.map((item) => ( +
+
+ +
+
+

{item.title}

+

+ {item.desc} +

+
+
+ ))} +
+
+ + {/* CTA */} +
+ {ready && !authenticated && ( +
+ +
+ )} + + {!ready && ( +
+
+

+ Initializing... +

+
+ )} +
+ + {/* Highlights */} +
+
+
+ What you get +
+
+
+ {highlights.map((item) => ( +
+
+ +
+
+

{item.title}

+

+ {item.desc} +

+
+ +
+ ))} +
+
+
+
+
+ ); +} diff --git a/community/privy-auth/src/components/login-button.tsx b/community/privy-auth/src/components/login-button.tsx new file mode 100644 index 00000000..e1c9e4ff --- /dev/null +++ b/community/privy-auth/src/components/login-button.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useLogin } from "@privy-io/react-auth"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { ArrowRight } from "lucide-react"; + +export function LoginButton() { + const router = useRouter(); + + const { login } = useLogin({ + onComplete: ({ user }) => { + if (user) { + router.replace("/dashboard"); + } + }, + onError: (error) => { + console.error("Login failed:", error); + }, + }); + + return ( + + ); +} diff --git a/community/privy-auth/src/components/providers.tsx b/community/privy-auth/src/components/providers.tsx new file mode 100644 index 00000000..923c1ab0 --- /dev/null +++ b/community/privy-auth/src/components/providers.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { PrivyProvider } from "@privy-io/react-auth"; +import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana"; + +const solanaConnectors = toSolanaWalletConnectors({ + shouldAutoConnect: true, +}); + +export function Providers({ children }: { children: React.ReactNode }) { + const { resolvedTheme } = useTheme(); + + return ( + + {children} + + ); +} diff --git a/community/privy-auth/src/components/sign-message.tsx b/community/privy-auth/src/components/sign-message.tsx new file mode 100644 index 00000000..6253af0f --- /dev/null +++ b/community/privy-auth/src/components/sign-message.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { useWallets, useSignMessage } from "@privy-io/react-auth/solana"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { PenLine, Copy, Loader2 } from "lucide-react"; + +export function SignMessage() { + const { wallets } = useWallets(); + const { signMessage } = useSignMessage(); + const embedded = wallets.find((w) => w.standardWallet.name === "Privy") ?? null; + const [signature, setSignature] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSign = async () => { + if (!embedded) return; + + setLoading(true); + setSignature(null); + + try { + const message = new TextEncoder().encode( + "Hello from privy-auth! This is a demo message signed with your embedded Solana wallet.", + ); + + const { signature: result } = await signMessage({ + message, + wallet: embedded, + }); + + const sig = Buffer.from(result).toString("base64"); + setSignature(sig); + toast.success("Message signed!"); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to sign message", + ); + } finally { + setLoading(false); + } + }; + + return ( + + +
+
+ +
+
+ Sign Message + + {embedded + ? "Sign a demo message with your embedded wallet." + : "Waiting for embedded wallet..."} + +
+
+
+ + + + {signature && ( +
+

+ Signature +

+ +
+ )} +
+
+ ); +} diff --git a/community/privy-auth/src/components/theme-toggle.tsx b/community/privy-auth/src/components/theme-toggle.tsx new file mode 100644 index 00000000..91b51c5a --- /dev/null +++ b/community/privy-auth/src/components/theme-toggle.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; +import { Moon, Sun } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) { + return ( + + ); + } + + return ( + + ); +} diff --git a/community/privy-auth/src/components/ui/badge.tsx b/community/privy-auth/src/components/ui/badge.tsx new file mode 100644 index 00000000..e87d62bf --- /dev/null +++ b/community/privy-auth/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/community/privy-auth/src/components/ui/button.tsx b/community/privy-auth/src/components/ui/button.tsx new file mode 100644 index 00000000..65d4fcd9 --- /dev/null +++ b/community/privy-auth/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/community/privy-auth/src/components/ui/card.tsx b/community/privy-auth/src/components/ui/card.tsx new file mode 100644 index 00000000..cabfbfc5 --- /dev/null +++ b/community/privy-auth/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/community/privy-auth/src/components/ui/separator.tsx b/community/privy-auth/src/components/ui/separator.tsx new file mode 100644 index 00000000..12d81c4a --- /dev/null +++ b/community/privy-auth/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/community/privy-auth/src/components/ui/sonner.tsx b/community/privy-auth/src/components/ui/sonner.tsx new file mode 100644 index 00000000..452f4d9f --- /dev/null +++ b/community/privy-auth/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/community/privy-auth/src/components/ui/tooltip.tsx b/community/privy-auth/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..28e19183 --- /dev/null +++ b/community/privy-auth/src/components/ui/tooltip.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/community/privy-auth/src/components/user-profile.tsx b/community/privy-auth/src/components/user-profile.tsx new file mode 100644 index 00000000..f58b0cd8 --- /dev/null +++ b/community/privy-auth/src/components/user-profile.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { usePrivy } from "@privy-io/react-auth"; +import { useWallets } from "@privy-io/react-auth/solana"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { toast } from "sonner"; +import { Copy, LogOut, User } from "lucide-react"; + +function truncate(str: string) { + return `${str.slice(0, 6)}...${str.slice(-4)}`; +} + +export function UserProfile() { + const { user, logout } = usePrivy(); + const { wallets } = useWallets(); + const embedded = wallets.find((w) => w.standardWallet.name === "Privy") ?? null; + const external = wallets.filter((w) => w.standardWallet.name !== "Privy"); + + if (!user) return null; + + const linkedMethods = user.linkedAccounts + .map((a) => a.type) + .filter((t) => t !== "wallet" && t !== "smart_wallet"); + + return ( + + +
+
+ +
+ Profile +
+ +
+ +
+

+ User ID +

+

{truncate(user.id)}

+
+ + {embedded && ( + <> + +
+

+ Embedded Wallet +

+ +
+ + )} + + {external.length > 0 && ( + <> + +
+

+ External Wallets +

+
+ {external.map((w) => ( +
+ + {truncate(w.address)} + + + {w.standardWallet.name} + +
+ ))} +
+
+ + )} + + {linkedMethods.length > 0 && ( + <> + +
+

+ Linked Accounts +

+
+ {linkedMethods.map((method) => ( + + {method.replace("_oauth", "")} + + ))} +
+
+ + )} +
+
+ ); +} diff --git a/community/privy-auth/src/components/wallet-info.tsx b/community/privy-auth/src/components/wallet-info.tsx new file mode 100644 index 00000000..cec5830d --- /dev/null +++ b/community/privy-auth/src/components/wallet-info.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { usePrivy } from "@privy-io/react-auth"; +import { useWallets } from "@privy-io/react-auth/solana"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Activity } from "lucide-react"; + +function StatusDot({ active }: { active: boolean }) { + return ( + + {active && ( + + )} + + + ); +} + +export function WalletInfo() { + const { authenticated } = usePrivy(); + const { wallets, ready } = useWallets(); + const embedded = wallets.find((w) => w.standardWallet.name === "Privy") ?? null; + + if (!ready) { + return ( + + + Initializing... + + + ); + } + + return ( + + +
+
+ +
+ Wallet Status +
+
+ +
+ + + {authenticated ? "Authenticated" : "Not authenticated"} + +
+
+ + + {embedded ? "Embedded wallet active" : "No embedded wallet"} + +
+
+ 0} /> + + {wallets.length} wallet{wallets.length !== 1 ? "s" : ""} connected + +
+
+
+ ); +} diff --git a/community/privy-auth/src/lib/utils.ts b/community/privy-auth/src/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/community/privy-auth/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/community/privy-auth/tailwind.config.ts b/community/privy-auth/tailwind.config.ts new file mode 100644 index 00000000..76f9ee39 --- /dev/null +++ b/community/privy-auth/tailwind.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./src/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; + +export default config; diff --git a/community/privy-auth/tsconfig.json b/community/privy-auth/tsconfig.json new file mode 100644 index 00000000..b8cdcc79 --- /dev/null +++ b/community/privy-auth/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}