@thunder-stack/create-thunder-app 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/index.js +229 -0
  2. package/package.json +26 -0
  3. package/postpublish.js +14 -0
  4. package/prepublish.js +68 -0
  5. package/template/.env.example +13 -0
  6. package/template/README.md +1 -0
  7. package/template/THUNDER_STACK.md +120 -0
  8. package/template/client/assets/logo.svg +36 -0
  9. package/template/client/expo/App.test.tsx +20 -0
  10. package/template/client/expo/App.tsx +16 -0
  11. package/template/client/expo/app.json +30 -0
  12. package/template/client/expo/babel.config.js +12 -0
  13. package/template/client/expo/global.css +3 -0
  14. package/template/client/expo/jest.config.js +7 -0
  15. package/template/client/expo/metro.config.js +21 -0
  16. package/template/client/expo/package.json +33 -0
  17. package/template/client/expo/tsconfig.json +6 -0
  18. package/template/client/next/app/globals.css +14 -0
  19. package/template/client/next/app/layout.tsx +28 -0
  20. package/template/client/next/app/page.test.tsx +13 -0
  21. package/template/client/next/app/page.tsx +10 -0
  22. package/template/client/next/next-env.d.ts +5 -0
  23. package/template/client/next/next.config.js +21 -0
  24. package/template/client/next/package.json +39 -0
  25. package/template/client/next/playwright.config.ts +17 -0
  26. package/template/client/next/postcss.config.js +6 -0
  27. package/template/client/next/tsconfig.json +30 -0
  28. package/template/client/next/vitest.config.ts +11 -0
  29. package/template/client/next/vitest.setup.ts +1 -0
  30. package/template/client/shared/index.ts +2 -0
  31. package/template/client/shared/package.json +24 -0
  32. package/template/client/shared/src/components/Button.tsx +38 -0
  33. package/template/client/shared/src/components/Card.tsx +17 -0
  34. package/template/client/shared/src/features/auth/login.tsx +139 -0
  35. package/template/client/shared/src/features/dashboard/index.tsx +232 -0
  36. package/template/client/shared/src/features/home/screen.tsx +212 -0
  37. package/template/client/shared/src/features/management/roles.tsx +356 -0
  38. package/template/client/shared/src/features/management/users.tsx +245 -0
  39. package/template/client/shared/src/nativewind-env.d.ts +1 -0
  40. package/template/client/shared/src/utils/auth.ts +21 -0
  41. package/template/client/shared/tailwind.config.js +19 -0
  42. package/template/client/shared/tsconfig.json +9 -0
  43. package/template/package.json +47 -0
  44. package/template/packages/tsconfig/base.json +20 -0
  45. package/template/packages/tsconfig/package.json +11 -0
  46. package/template/pnpm-workspace.yaml +4 -0
  47. package/template/scripts/clean.js +56 -0
  48. package/template/server/db/drizzle.config.ts +10 -0
  49. package/template/server/db/migrations/0000_loving_mindworm.sql +86 -0
  50. package/template/server/db/migrations/meta/0000_snapshot.json +549 -0
  51. package/template/server/db/migrations/meta/_journal.json +13 -0
  52. package/template/server/db/package.json +28 -0
  53. package/template/server/db/src/index.ts +16 -0
  54. package/template/server/db/src/migrate.ts +28 -0
  55. package/template/server/db/src/schema/auth.ts +73 -0
  56. package/template/server/db/src/schema/index.ts +2 -0
  57. package/template/server/db/src/schema/rbac.ts +72 -0
  58. package/template/server/db/src/schema.ts +1 -0
  59. package/template/server/db/src/seed.ts +204 -0
  60. package/template/server/db/src/services/rbac.ts +144 -0
  61. package/template/server/db/src/services/user.ts +34 -0
  62. package/template/server/db/src/types/index.ts +34 -0
  63. package/template/server/db/tsconfig.json +8 -0
  64. package/template/server/hono/package.json +22 -0
  65. package/template/server/hono/src/auth.ts +29 -0
  66. package/template/server/hono/src/index.ts +215 -0
  67. package/template/server/hono/tsconfig.json +8 -0
  68. package/template/server/hono/wrangler.toml +12 -0
  69. package/template/turbo.json +68 -0
@@ -0,0 +1,14 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html,
6
+ body,
7
+ #__next {
8
+ width: 100%;
9
+ height: 100%;
10
+ margin: 0;
11
+ padding: 0;
12
+ background-color: #090d16;
13
+ color: #f8fafc;
14
+ }
@@ -0,0 +1,28 @@
1
+ import "./globals.css";
2
+ import type { Metadata } from "next";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "THUNDER Stack • Cross-Platform Hub",
6
+ description: "Universal web and mobile sign-in platform built with Next.js, Expo, and Solito.",
7
+ icons: {
8
+ icon: "/logo.svg",
9
+ shortcut: "/logo.svg",
10
+ apple: "/logo.svg",
11
+ },
12
+ };
13
+
14
+ export default function RootLayout({
15
+ children,
16
+ }: {
17
+ children: React.ReactNode;
18
+ }) {
19
+ return (
20
+ <html lang="en" className="h-full bg-slate-950">
21
+ <body className="h-full font-sans antialiased text-slate-200">
22
+ <main id="app-root" className="h-full">
23
+ {children}
24
+ </main>
25
+ </body>
26
+ </html>
27
+ );
28
+ }
@@ -0,0 +1,13 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import Page from "./page";
3
+ import { expect, test, vi } from "vitest";
4
+
5
+ // Mock the shared Solito screen to isolate wrapper test
6
+ vi.mock("@thunder/app", () => ({
7
+ HomeScreen: () => <div>Thunder Test</div>,
8
+ }));
9
+
10
+ test("renders the Next.js page wrapper with the shared home screen", () => {
11
+ render(<Page />);
12
+ expect(screen.getByText("Thunder Test")).toBeInTheDocument();
13
+ });
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ import { HomeScreen } from "@thunder/app";
4
+
5
+ // Force Edge Serverless Runtime for deployment to Vercel/Cloudflare Pages
6
+ export const runtime = "edge";
7
+
8
+ export default function Page() {
9
+ return <HomeScreen />;
10
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
@@ -0,0 +1,21 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ transpilePackages: [
4
+ "react-native",
5
+ "react-native-web",
6
+ "solito",
7
+ "@thunder/app",
8
+ "moti",
9
+ "react-native-reanimated"
10
+ ],
11
+ webpack: (config) => {
12
+ config.resolve.alias = {
13
+ ...(config.resolve.alias || {}),
14
+ // Transform direct react-native imports to react-native-web
15
+ "react-native$": "react-native-web",
16
+ };
17
+ return config;
18
+ },
19
+ };
20
+
21
+ module.exports = nextConfig;
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "next-web-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "pages:build": "opennextjs-cloudflare build",
10
+ "test:unit": "vitest run",
11
+ "test:e2e": "playwright test"
12
+ },
13
+ "dependencies": {
14
+ "@thunder/app": "workspace:*",
15
+ "moti": "^0.28.1",
16
+ "next": "^14.2.35",
17
+ "react": "^18.3.1",
18
+ "react-dom": "^18.3.1",
19
+ "react-native-reanimated": "~3.10.1",
20
+ "react-native-web": "^0.19.13"
21
+ },
22
+ "devDependencies": {
23
+ "@opennextjs/cloudflare": "^1.20.1",
24
+ "@thunder/tsconfig": "workspace:*",
25
+ "@playwright/test": "^1.61.1",
26
+ "@testing-library/jest-dom": "^6.9.1",
27
+ "@testing-library/react": "^14.3.1",
28
+ "@types/node": "^20.19.43",
29
+ "@types/react": "^18.3.31",
30
+ "@types/react-dom": "^18.3.7",
31
+ "@vitejs/plugin-react": "^4.7.0",
32
+ "autoprefixer": "^10.5.2",
33
+ "jsdom": "^26.1.0",
34
+ "postcss": "^8.5.16",
35
+ "tailwindcss": "^3.4.19",
36
+ "typescript": "^5.9.3",
37
+ "vitest": "^3.2.7"
38
+ }
39
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./e2e",
5
+ fullyParallel: true,
6
+ reporter: "html",
7
+ use: {
8
+ baseURL: "http://localhost:3000",
9
+ trace: "on-first-retry",
10
+ },
11
+ projects: [
12
+ {
13
+ name: "chromium",
14
+ use: { ...devices["Desktop Chrome"] },
15
+ },
16
+ ],
17
+ });
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "extends": "@thunder/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "noEmit": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "jsx": "preserve",
14
+ "plugins": [
15
+ {
16
+ "name": "next"
17
+ }
18
+ ],
19
+ "incremental": true
20
+ },
21
+ "include": [
22
+ "next-env.d.ts",
23
+ "**/*.ts",
24
+ "**/*.tsx",
25
+ ".next/types/**/*.ts"
26
+ ],
27
+ "exclude": [
28
+ "node_modules"
29
+ ]
30
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: "jsdom",
8
+ globals: true,
9
+ setupFiles: "./vitest.setup.ts",
10
+ },
11
+ });
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -0,0 +1,2 @@
1
+ export * from "./src/features/home/screen";
2
+ export * from "./src/utils/auth";
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@thunder/app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "index.ts",
6
+ "dependencies": {
7
+ "@react-native-async-storage/async-storage": "^1.24.0",
8
+ "better-auth": "^1.6.23",
9
+ "moti": "^0.28.1",
10
+ "nativewind": "^4.2.6",
11
+ "react-native": "*",
12
+ "react-native-reanimated": "~3.10.1",
13
+ "solito": "^4.4.1"
14
+ },
15
+ "peerDependencies": {
16
+ "react": "*"
17
+ },
18
+ "devDependencies": {
19
+ "@thunder/tsconfig": "workspace:*",
20
+ "@types/node": "^20.19.43",
21
+ "@types/react": "^18.3.31",
22
+ "react": "^18.3.1"
23
+ }
24
+ }
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { Pressable, Text, ActivityIndicator } from "react-native";
3
+
4
+ interface ButtonProps {
5
+ title: string;
6
+ onPress: () => void;
7
+ loading?: boolean;
8
+ variant?: "primary" | "secondary" | "danger";
9
+ }
10
+
11
+ export function Button({ title, onPress, loading = false, variant = "primary" }: ButtonProps) {
12
+ let containerStyle = "px-6 py-4 rounded-2xl flex-row items-center justify-center shadow-lg ";
13
+ let textStyle = "font-bold text-base tracking-wide ";
14
+
15
+ if (variant === "primary") {
16
+ containerStyle += "bg-blue-600 active:bg-blue-700 shadow-blue-500/20";
17
+ textStyle += "text-white";
18
+ } else if (variant === "secondary") {
19
+ containerStyle += "bg-slate-800 border border-slate-700 active:bg-slate-750 shadow-black/40";
20
+ textStyle += "text-slate-200";
21
+ } else {
22
+ containerStyle += "bg-rose-600 active:bg-rose-700 shadow-rose-500/20";
23
+ textStyle += "text-white";
24
+ }
25
+
26
+ return (
27
+ <Pressable
28
+ className={containerStyle}
29
+ onPress={loading ? undefined : onPress}
30
+ disabled={loading}
31
+ >
32
+ {loading ? (
33
+ <ActivityIndicator size="small" color="#ffffff" className="mr-2" />
34
+ ) : null}
35
+ <Text className={textStyle}>{title}</Text>
36
+ </Pressable>
37
+ );
38
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+
4
+ interface CardProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function Card({ children, className = "" }: CardProps) {
10
+ return (
11
+ <View
12
+ className={`bg-slate-900/60 border border-slate-800/80 p-8 rounded-3xl w-full max-w-md shadow-2xl backdrop-blur-md ${className}`}
13
+ >
14
+ {children}
15
+ </View>
16
+ );
17
+ }
@@ -0,0 +1,139 @@
1
+ import React, { useState } from "react";
2
+ import { View, Text, TextInput, ActivityIndicator, Platform, TouchableOpacity } from "react-native";
3
+ import { Button } from "../../components/Button";
4
+ import { Card } from "../../components/Card";
5
+ import { authClient } from "../../utils/auth";
6
+
7
+ interface LoginProps {
8
+ onSuccess: () => void;
9
+ }
10
+
11
+ export function LoginScreen({ onSuccess }: LoginProps) {
12
+ const [email, setEmail] = useState("");
13
+ const [password, setPassword] = useState("");
14
+ const [loading, setLoading] = useState(false);
15
+ const [error, setError] = useState("");
16
+
17
+ const handleEmailSignIn = async () => {
18
+ if (!email || !password) {
19
+ setError("Please enter both email and password.");
20
+ return;
21
+ }
22
+
23
+ setLoading(true);
24
+ setError("");
25
+
26
+ try {
27
+ const response = await authClient.signIn.email({
28
+ email,
29
+ password,
30
+ });
31
+ if (response?.error) {
32
+ setError(response.error.message || "Invalid email or password.");
33
+ } else {
34
+ onSuccess();
35
+ }
36
+ } catch (err: any) {
37
+ setError(err?.message || "An unexpected error occurred during login.");
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+
43
+ const handleGoogleSignIn = async () => {
44
+ setLoading(true);
45
+ setError("");
46
+ try {
47
+ const callbackURL = Platform.select({
48
+ web: typeof window !== "undefined" ? window.location.origin : "http://localhost:3000",
49
+ default: "exp://localhost:8081",
50
+ });
51
+
52
+ await authClient.signIn.social({
53
+ provider: "google",
54
+ callbackURL,
55
+ });
56
+ } catch (err: any) {
57
+ setError(err?.message || "Google Authentication failed.");
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <View className="w-full max-w-md px-4">
64
+ <Card className="border-slate-800 bg-slate-900/60 backdrop-blur-xl p-8 rounded-3xl shadow-2xl border">
65
+ <Text className="text-white text-3xl font-black mb-2 text-center tracking-tight">
66
+ Welcome Back
67
+ </Text>
68
+ <Text className="text-slate-400 text-sm font-medium mb-8 text-center">
69
+ Secure sign-in for Web and Mobile
70
+ </Text>
71
+
72
+ {error ? (
73
+ <View className="mb-6 p-4 bg-red-950/40 border border-red-500/20 rounded-xl">
74
+ <Text className="text-red-400 text-xs font-semibold text-center">{error}</Text>
75
+ </View>
76
+ ) : null}
77
+
78
+ <View className="gap-5">
79
+ <View>
80
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
81
+ Email Address
82
+ </Text>
83
+ <TextInput
84
+ className="w-full bg-slate-950/60 border border-slate-800 text-white rounded-xl px-4 py-3 text-sm font-medium focus:border-blue-500"
85
+ placeholder="e.g. admin@thunderstack.dev"
86
+ placeholderTextColor="#64748b"
87
+ value={email}
88
+ onChangeText={setEmail}
89
+ autoCapitalize="none"
90
+ keyboardType="email-address"
91
+ />
92
+ </View>
93
+
94
+ <View className="mb-2">
95
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
96
+ Password
97
+ </Text>
98
+ <TextInput
99
+ className="w-full bg-slate-950/60 border border-slate-800 text-white rounded-xl px-4 py-3 text-sm font-medium focus:border-blue-500"
100
+ placeholder="••••••••"
101
+ placeholderTextColor="#64748b"
102
+ secureTextEntry
103
+ value={password}
104
+ onChangeText={setPassword}
105
+ autoCapitalize="none"
106
+ />
107
+ </View>
108
+
109
+ {loading ? (
110
+ <ActivityIndicator size="small" color="#3b82f6" className="my-2" />
111
+ ) : (
112
+ <Button title="Sign In" onPress={handleEmailSignIn} variant="primary" />
113
+ )}
114
+
115
+ <View className="flex-row items-center my-4">
116
+ <View className="flex-1 h-[1px] bg-slate-800" />
117
+ <Text className="text-slate-500 text-xs font-bold uppercase mx-4">or</Text>
118
+ <View className="flex-1 h-[1px] bg-slate-800" />
119
+ </View>
120
+
121
+ <Button
122
+ title="Continue with Google"
123
+ onPress={handleGoogleSignIn}
124
+ variant="secondary"
125
+ />
126
+ </View>
127
+
128
+ {/* Admin Creds Helper Info */}
129
+ <View className="mt-8 pt-6 border-t border-slate-800/60">
130
+ <Text className="text-slate-500 text-[10px] text-center font-medium leading-4">
131
+ Admin Account Seeded:{"\n"}
132
+ <Text className="text-blue-500 font-mono select-all">admin@thunderstack.dev</Text> /{" "}
133
+ <Text className="text-blue-500 font-mono select-all">AdminPassword123!</Text>
134
+ </Text>
135
+ </View>
136
+ </Card>
137
+ </View>
138
+ );
139
+ }
@@ -0,0 +1,232 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { View as MotiView } from "moti";
3
+ import { View, Text, ActivityIndicator, ScrollView, RefreshControl, Platform } from "react-native";
4
+ import { Card } from "../../components/Card";
5
+ import { Button } from "../../components/Button";
6
+ import { authClient } from "../../utils/auth";
7
+
8
+ interface DashboardProps {
9
+ user: any;
10
+ roles: string[];
11
+ permissions: string[];
12
+ }
13
+
14
+ function SkeletonCard({ delay }: { delay: number }) {
15
+ return (
16
+ <MotiView
17
+ className="flex-1 min-w-[200px]"
18
+ from={{ opacity: 0.3, scale: 0.95 }}
19
+ animate={{ opacity: 0.7, scale: 1 }}
20
+ transition={{
21
+ type: "timing",
22
+ duration: 900,
23
+ loop: true,
24
+ delay,
25
+ }}
26
+ >
27
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border min-h-[110px]">
28
+ <View className="h-4 w-24 bg-slate-800 rounded mb-4" />
29
+ <View className="h-8 w-12 bg-slate-800 rounded" />
30
+ </Card>
31
+ </MotiView>
32
+ );
33
+ }
34
+
35
+ export function DashboardView({ user, roles, permissions }: DashboardProps) {
36
+ const [stats, setStats] = useState<{ users: number; roles: number; permissions: number } | null>(null);
37
+ const [loading, setLoading] = useState(true);
38
+ const [refreshing, setRefreshing] = useState(false);
39
+
40
+ const fetchStats = async () => {
41
+ try {
42
+ const res = await fetch(`${authClient.baseURL}/api/dashboard/stats`, {
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ },
46
+ });
47
+ const data = await res.json();
48
+ if (!data.error) {
49
+ setStats(data);
50
+ }
51
+ } catch (err) {
52
+ console.error("Failed to load dashboard stats:", err);
53
+ } finally {
54
+ setLoading(false);
55
+ setRefreshing(false);
56
+ }
57
+ };
58
+
59
+ useEffect(() => {
60
+ fetchStats();
61
+ }, []);
62
+
63
+ const onRefresh = () => {
64
+ setRefreshing(true);
65
+ fetchStats();
66
+ };
67
+
68
+ return (
69
+ <ScrollView
70
+ className="flex-1"
71
+ refreshControl={
72
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#3b82f6" />
73
+ }
74
+ >
75
+ <View className="p-6 gap-8">
76
+ {/* Welcome Banner */}
77
+ <MotiView
78
+ from={{ opacity: 0, translateY: -20 }}
79
+ animate={{ opacity: 1, translateY: 0 }}
80
+ transition={{ type: "timing", duration: 400 }}
81
+ >
82
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-widest mb-1">
83
+ Overview
84
+ </Text>
85
+ <Text className="text-white text-3xl font-black tracking-tight">
86
+ Welcome back, <Text className="text-yellow-500">{user.name}</Text>
87
+ </Text>
88
+ </MotiView>
89
+
90
+ {/* Stats Grid */}
91
+ <View className="flex-row flex-wrap gap-4">
92
+ {loading ? (
93
+ <>
94
+ <SkeletonCard delay={0} />
95
+ <SkeletonCard delay={150} />
96
+ <SkeletonCard delay={300} />
97
+ </>
98
+ ) : (
99
+ <>
100
+ <MotiView
101
+ className="flex-1 min-w-[200px]"
102
+ from={{ opacity: 0, scale: 0.95 }}
103
+ animate={{ opacity: 1, scale: 1 }}
104
+ transition={{ type: "timing", duration: 500, delay: 100 }}
105
+ >
106
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border relative overflow-hidden">
107
+ <View className="absolute -right-4 -bottom-4 w-24 h-24 bg-blue-500/10 rounded-full blur-xl" />
108
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
109
+ Total Users
110
+ </Text>
111
+ <Text className="text-white text-4xl font-extrabold tracking-tight">
112
+ {stats?.users ?? 0}
113
+ </Text>
114
+ </Card>
115
+ </MotiView>
116
+
117
+ <MotiView
118
+ className="flex-1 min-w-[200px]"
119
+ from={{ opacity: 0, scale: 0.95 }}
120
+ animate={{ opacity: 1, scale: 1 }}
121
+ transition={{ type: "timing", duration: 500, delay: 200 }}
122
+ >
123
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border relative overflow-hidden">
124
+ <View className="absolute -right-4 -bottom-4 w-24 h-24 bg-indigo-500/10 rounded-full blur-xl" />
125
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
126
+ Active Roles
127
+ </Text>
128
+ <Text className="text-white text-4xl font-extrabold tracking-tight">
129
+ {stats?.roles ?? 0}
130
+ </Text>
131
+ </Card>
132
+ </MotiView>
133
+
134
+ <MotiView
135
+ className="flex-1 min-w-[200px]"
136
+ from={{ opacity: 0, scale: 0.95 }}
137
+ animate={{ opacity: 1, scale: 1 }}
138
+ transition={{ type: "timing", duration: 500, delay: 300 }}
139
+ >
140
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border relative overflow-hidden">
141
+ <View className="absolute -right-4 -bottom-4 w-24 h-24 bg-purple-500/10 rounded-full blur-xl" />
142
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
143
+ Permissions Enabled
144
+ </Text>
145
+ <Text className="text-white text-4xl font-extrabold tracking-tight">
146
+ {stats?.permissions ?? 0}
147
+ </Text>
148
+ </Card>
149
+ </MotiView>
150
+ </>
151
+ )}
152
+ </View>
153
+
154
+ {/* User Roles & Permissions Info */}
155
+ <MotiView
156
+ from={{ opacity: 0, translateY: 20 }}
157
+ animate={{ opacity: 1, translateY: 0 }}
158
+ transition={{ type: "timing", duration: 500, delay: 400 }}
159
+ >
160
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border">
161
+ <Text className="text-white text-lg font-bold mb-4">Your Security Profile</Text>
162
+
163
+ <View className="gap-6">
164
+ <View>
165
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
166
+ Assigned Roles
167
+ </Text>
168
+ <View className="flex-row flex-wrap gap-2">
169
+ {roles.length > 0 ? (
170
+ roles.map((role) => (
171
+ <View key={role} className="bg-blue-500/10 border border-blue-500/25 px-3 py-1 rounded-full">
172
+ <Text className="text-blue-400 text-xs font-bold">{role}</Text>
173
+ </View>
174
+ ))
175
+ ) : (
176
+ <Text className="text-slate-500 text-sm font-medium">No roles assigned.</Text>
177
+ )}
178
+ </View>
179
+ </View>
180
+
181
+ <View>
182
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-wider mb-2">
183
+ Permissions Matrix
184
+ </Text>
185
+ <View className="flex-row flex-wrap gap-2">
186
+ {permissions.length > 0 ? (
187
+ permissions.map((perm) => (
188
+ <View key={perm} className="bg-emerald-500/10 border border-emerald-500/25 px-3 py-1 rounded-full">
189
+ <Text className="text-emerald-400 text-xs font-semibold">{perm}</Text>
190
+ </View>
191
+ ))
192
+ ) : (
193
+ <Text className="text-slate-500 text-sm font-medium">No permissions granted.</Text>
194
+ )}
195
+ </View>
196
+ </View>
197
+ </View>
198
+ </Card>
199
+ </MotiView>
200
+
201
+ {/* Auth Cookie Details Card */}
202
+ <MotiView
203
+ from={{ opacity: 0, translateY: 20 }}
204
+ animate={{ opacity: 1, translateY: 0 }}
205
+ transition={{ type: "timing", duration: 500, delay: 500 }}
206
+ >
207
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border">
208
+ <Text className="text-white text-lg font-bold mb-2">Session Information</Text>
209
+ <Text className="text-slate-400 text-xs font-medium mb-4">
210
+ Secure HTTP cookies safeguard client sessions with Cross-Site scripting prevention.
211
+ </Text>
212
+
213
+ <View className="bg-slate-950/60 p-4 rounded-xl border border-slate-800/80 gap-3">
214
+ <View className="flex-row justify-between items-center">
215
+ <Text className="text-slate-400 text-xs font-bold">API Base Endpoint</Text>
216
+ <Text className="text-slate-300 text-xs font-mono">{authClient.baseURL}</Text>
217
+ </View>
218
+ <View className="flex-row justify-between items-center">
219
+ <Text className="text-slate-400 text-xs font-bold">Environment</Text>
220
+ <Text className="text-blue-400 text-xs font-semibold uppercase">{Platform.OS}</Text>
221
+ </View>
222
+ <View className="flex-row justify-between items-center">
223
+ <Text className="text-slate-400 text-xs font-bold">Verification State</Text>
224
+ <Text className="text-emerald-400 text-xs font-bold">Encrypted</Text>
225
+ </View>
226
+ </View>
227
+ </Card>
228
+ </MotiView>
229
+ </View>
230
+ </ScrollView>
231
+ );
232
+ }