Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/dev-launchpad/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ <h2 style="margin-top: 64px;">Background services</h2>
img: "https://www.svgrepo.com/show/448400/docs.svg",
importance: 2,
},
{
name: "Hosted Components",
portSuffix: "09",
description: [
"Src: ./apps/hosted-components",
],
importance: 2,
},
{
name: "Inbucket",
portSuffix: "05",
Expand Down
1 change: 1 addition & 0 deletions apps/hosted-components/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
4 changes: 4 additions & 0 deletions apps/hosted-components/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
extends: ["../../configs/eslint/defaults.js"],
ignorePatterns: ["/*", "!/src"],
};
37 changes: 37 additions & 0 deletions apps/hosted-components/package.json
Comment thread
BilalG1 marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also create a vercel project for this? domain built-with-stack-auth.com is on cloudflare

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@stackframe/hosted-components",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09",
Comment thread
BilalG1 marked this conversation as resolved.
"build": "vite build",
"start": "node .output/server/index.mjs",
"lint": "eslint --ext .ts,.tsx .",
"typecheck": "tsc --noEmit",
"clean": "rimraf .output && rimraf node_modules"
},
Comment thread
BilalG1 marked this conversation as resolved.
"dependencies": {
"@stackframe/react": "workspace:*",
Comment thread
BilalG1 marked this conversation as resolved.
"@stackframe/stack-shared": "workspace:*",
"@tanstack/react-router": "^1.121.3",
"@tanstack/react-start": "^1.121.3",
"@tanstack/react-start-client": "^1.121.3",
"@tanstack/react-start-server": "^1.121.3",
"@tanstack/start-client-core": "^1.121.3",
"@tanstack/start-server-core": "^1.121.3",
"@tanstack/start-plugin-core": "^1.121.3",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@types/node": "^22.13.0",
"@types/react": "19.2.1",
"@types/react-dom": "19.2.1",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "5.9.3",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^4.3.2"
},
"packageManager": "pnpm@10.23.0"
}
12 changes: 12 additions & 0 deletions apps/hosted-components/src/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { StartClient } from '@tanstack/react-start/client';
import { StrictMode, startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<StartClient />
</StrictMode>,
);
});
86 changes: 86 additions & 0 deletions apps/hosted-components/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as HandlerSplatRouteImport } from './routes/handler/$'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const HandlerSplatRoute = HandlerSplatRouteImport.update({
id: '/handler/$',
path: '/handler/$',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/handler/$': typeof HandlerSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/handler/$': typeof HandlerSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/handler/$': typeof HandlerSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/handler/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/handler/$'
id: '__root__' | '/' | '/handler/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
HandlerSplatRoute: typeof HandlerSplatRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/handler/$': {
id: '/handler/$'
path: '/handler/$'
fullPath: '/handler/$'
preLoaderRoute: typeof HandlerSplatRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
HandlerSplatRoute: HandlerSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
12 changes: 12 additions & 0 deletions apps/hosted-components/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
});

return router;
}

137 changes: 137 additions & 0 deletions apps/hosted-components/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/// <reference types="vite/client" />
import { StackClientApp, StackProvider, StackTheme } from '@stackframe/react';
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
useNavigate
} from '@tanstack/react-router';
import type { ErrorInfo, ReactNode } from 'react';
import { Component, useEffect, useMemo, useState } from 'react';


export function getProjectId(): string | null {
// Extract from subdomain: <projectId>.built-with-stack-auth.com
// Also works with <projectId>.localhost for local dev
const hostname = window.location.hostname;
const parts = hostname.split('.');
if (parts.length >= 2) {
return parts[0];
}

return null;
}

function FullPageError({ title, message }: { title: string, message: string }) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<div style={{ textAlign: 'center', maxWidth: 480, padding: 24 }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>{title}</h1>
<p style={{ color: '#666' }}>{message}</p>
</div>
</div>
);
}

class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error: Error) {
return { error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Hosted components error:', error, errorInfo);
}

render() {
if (this.state.error) {
return <FullPageError title="Something went wrong" message={this.state.error.message} />;
}

return this.props.children;
}
}

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
}),
shellComponent: RootDocument,
component: RootComponent,
});

function RootDocument({ children }: { children: ReactNode }) {
return (
<html>
<head>
<HeadContent />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body style={{ fontFamily: "'Inter', sans-serif", margin: 0 }}>
{children}
<Scripts />
</body>
</html>
);
}

function RootComponent() {
const [projectId, setProjectId] = useState<string | null | undefined>("internal");

useEffect(() => {
setProjectId(getProjectId());
}, []);
Comment thread
BilalG1 marked this conversation as resolved.

const isValidProjectId = projectId ? (projectId === "internal" || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) : false;
Comment thread
BilalG1 marked this conversation as resolved.

const stackApp = useMemo(() => {
if (!projectId || !isValidProjectId) return null;
return new StackClientApp({
projectId,
tokenStore: "cookie",
baseUrl: import.meta.env.VITE_STACK_API_URL || undefined,
urls: {
handler: "/handler",
signIn: "/handler/sign-in",
signUp: "/handler/sign-up",
afterSignIn: "/",
afterSignUp: "/",
afterSignOut: "/handler/sign-in",
},
redirectMethod: { useNavigate: useNavigate as any }
});
}, [projectId]);
Comment thread
BilalG1 marked this conversation as resolved.

if (projectId === undefined) {
return <></>;
}

if (!projectId) {
return <FullPageError title="Invalid URL" message={`Could not determine project ID from subdomain. Visit <projectId>.${window.location.host}.`} />;
}

if (!isValidProjectId) {
return <FullPageError title="Something went wrong" message={`Invalid project ID: ${projectId}. Project IDs must be UUIDs.`} />;
}

return (
<ErrorBoundary>
<StackProvider app={stackApp!}>
<StackTheme>
<Outlet />
</StackTheme>
</StackProvider>
</ErrorBoundary>
);
}

14 changes: 14 additions & 0 deletions apps/hosted-components/src/routes/handler/$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router';
import { StackHandler } from '@stackframe/react';
import { useState, useEffect } from 'react';

export const Route = createFileRoute('/handler/$')({
component: HandlerPage,
});

function HandlerPage() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => setIsMounted(true), []);
if (!isMounted) return null;
return <StackHandler fullPage />;
}
31 changes: 31 additions & 0 deletions apps/hosted-components/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { UserButton, useUser } from "@stackframe/react";
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/')({
component: HandlerPage,
pendingComponent: () => (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh" }}>
<div style={{ width: 24, height: 24, border: "2px solid #e5e5e5", borderTop: "2px solid #333", borderRadius: "50%", animation: "spin 0.6s linear infinite" }} />
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div>
Comment thread
BilalG1 marked this conversation as resolved.
),
});

function HandlerPage() {
const user = useUser({ or: "redirect" });
const name = user.displayName || user.primaryEmail || "User";

return (
<div style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", fontFamily: "system-ui, sans-serif" }}>
<div style={{ position: "absolute", top: "1rem", right: "1rem" }}>
<UserButton />
</div>
<h1 style={{ fontSize: "1.5rem", fontWeight: 500, marginBottom: "0.5rem" }}>
Welcome, {name}
</h1>
<p style={{ color: "#666", fontSize: "0.875rem" }}>
You are signed in.
</p>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/hosted-components/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2021",
"lib": ["DOM", "DOM.Iterable", "ES2021"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"paths": {
"~/*": ["./src/*"]
}
},
"include": ["src", "vite.config.ts"]
}
Loading