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
56 changes: 50 additions & 6 deletions app/(setup)/login/components/main-components/AuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ import { SignInForm } from '@/app/(setup)/login/components/sing-in/SignInForm';
import { SignUpForm } from '@/app/(setup)/login/components/sing-up/SignUpForm';
import { ForgotPasswordForm } from '@/app/(setup)/login/components/forgot-password/ForgotPasswordForm';
import { VerificationForm } from '@/app/(setup)/login/components/verification-form/VerificationForm';
import { signInWithRedirect } from 'aws-amplify/auth';
import { signInWithRedirect, AuthError } from 'aws-amplify/auth';
import { getSignInErrorMessage } from '@/lib/auth/auth-error-messages';

type AuthState = 'signin' | 'signup' | 'forgot-password' | 'verification';

export function AuthForm() {
const [authState, setAuthState] = useState<AuthState>('signin');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [googleError, setGoogleError] = useState<string | null>(null);

const clearGoogleError = useCallback(() => {
setGoogleError(null);
}, []);

const renderForm = useCallback(() => {
switch (authState) {
Expand All @@ -24,11 +30,13 @@ export function AuthForm() {
<SignInForm
onForgotPassword={() => {
setAuthState('forgot-password');
clearGoogleError();
}}
onVerificationNeeded={(email, password) => {
setEmail(email);
setPassword(password);
setAuthState('verification');
clearGoogleError();
}}
/>
);
Expand All @@ -39,15 +47,32 @@ export function AuthForm() {
setEmail(email);
setPassword(password);
setAuthState('verification');
clearGoogleError();
}}
/>
);
case 'forgot-password':
return <ForgotPasswordForm onBack={() => setAuthState('signin')} />;
return (
<ForgotPasswordForm
onBack={() => {
setAuthState('signin');
clearGoogleError();
}}
/>
);
case 'verification':
return <VerificationForm email={email} password={password} onBack={() => setAuthState('signup')} />;
return (
<VerificationForm
email={email}
password={password}
onBack={() => {
setAuthState('signup');
clearGoogleError();
}}
/>
);
}
}, [authState, email, password]);
}, [authState, email, password, clearGoogleError]);

const handleLoginClick = async () => {
try {
Expand All @@ -56,6 +81,10 @@ export function AuthForm() {
});
} catch (error) {
console.error('Error during sign-in:', error);

if (error instanceof AuthError) {
setGoogleError(getSignInErrorMessage(error));
}
}
};

Expand Down Expand Up @@ -117,6 +146,9 @@ export function AuthForm() {
</motion.div>
</AnimatePresence>

{/* Mostrar error de Google Auth */}
{googleError && <div className="p-3 rounded-md bg-red-50 text-red-500 text-sm">{googleError}</div>}

{(authState === 'signin' || authState === 'signup') && (
<>
<div className="relative">
Expand Down Expand Up @@ -155,14 +187,26 @@ export function AuthForm() {
{authState === 'signin' ? (
<>
¿No tienes una cuenta?{' '}
<button type="button" className="underline" onClick={() => setAuthState('signup')}>
<button
type="button"
className="underline"
onClick={() => {
setAuthState('signup');
clearGoogleError();
}}>
Regístrate
</button>
</>
) : (
<>
¿Ya tienes una cuenta?{' '}
<button type="button" className="underline" onClick={() => setAuthState('signin')}>
<button
type="button"
className="underline"
onClick={() => {
setAuthState('signin');
clearGoogleError();
}}>
Inicia sesión
</button>
</>
Expand Down
7 changes: 1 addition & 6 deletions app/(setup)/login/hooks/SignIn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resendSignUpCode, signIn, type SignInInput } from 'aws-amplify/auth';
import { resendSignUpCode, signIn, type SignInInput, AuthError } from 'aws-amplify/auth';
import { useRouter } from 'next/navigation';
import { useCallback, useState } from 'react';
import { useAuth } from '@/context/hooks/useAuth';
Expand All @@ -14,11 +14,6 @@ interface UseAuthReturn {
resendConfirmationCode: (email: string) => Promise<void>;
}

interface AuthError {
code: string;
message: string;
}

interface UseAuthProps {
redirectPath?: string;
onVerificationNeeded?: (email: string, password: string) => void;
Expand Down
86 changes: 16 additions & 70 deletions app/[store]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import DevAutoReloadScript from '@/app/[store]/components/DevAutoReloadScript';
import { generateStoreMetadata, getCachedRenderResult, isAssetPath } from '@/app/[store]/lib/store-page-utils';
import { logger } from '@fasttify/liquid-forge';
import { storeViewsTracker } from '@fasttify/liquid-forge';
import { domainResolver } from '@fasttify/liquid-forge';
import DevAutoReloadScript from '@/app/[store]/src/components/DevAutoReloadScript';
import { StorePageController } from '@/app/[store]/src/_lib/controllers/store-page-controller';
import { StoreMetadataController } from '@/app/[store]/src/_lib/controllers/store-metadata-controller';
import { isAssetPath } from '@/app/[store]/src/lib/store-page-utils';
import { Metadata } from 'next';
import { headers } from 'next/headers';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';

export const dynamic = 'force-dynamic';

/**
* Función cacheada usando React.cache() que persiste entre generateMetadata y StorePage
* Esta es la forma oficial de Next.js para compartir datos entre estas funciones
*/

interface StorePageProps {
params: Promise<{
store: string;
Expand All @@ -28,54 +19,14 @@ interface StorePageProps {
/**
* Página principal de tienda con SSR
* Maneja todas las rutas de tienda: /, /products/slug, /collections/slug, etc
*
* Esta página solo orquesta la llamada al controller, delegando toda la lógica
*/
export default async function StorePage({ params, searchParams }: StorePageProps) {
const requestHeaders = await headers();
const xOriginalHost = requestHeaders.get('x-original-host');

const hostname =
xOriginalHost ||
(requestHeaders.get('cf-connecting-ip')
? requestHeaders.get('x-forwarded-host') || requestHeaders.get('host') || ''
: requestHeaders.get('host') || '');

const cleanHostname = hostname?.split(':')[0] || '';

const isMainDomain =
cleanHostname === 'fasttify.com' || cleanHostname === 'www.fasttify.com' || cleanHostname === 'localhost';

if (isMainDomain) {
notFound();
}

const { store } = await params;
const awaitedSearchParams = await searchParams;
const path = awaitedSearchParams.path || '/';

if (isAssetPath(path)) {
notFound();
}
export default async function StorePage(props: StorePageProps) {
const controller = new StorePageController();

try {
const domain = store.includes('.') ? store : `${store}.fasttify.com`;

const result = await getCachedRenderResult(domain, path, awaitedSearchParams);

// Trackear la vista de la tienda de forma asíncrona
try {
const headersObj = Object.fromEntries(requestHeaders.entries());
const fullUrl = `https://${cleanHostname}${path}`;

const storeRecord = await domainResolver.resolveStoreByDomain(domain);
const realStoreId = storeRecord.storeId;

const viewData = storeViewsTracker.captureStoreView(realStoreId, path, headersObj, fullUrl);
storeViewsTracker.trackStoreView(viewData).catch((trackError) => {
logger.error(`[StorePage] Failed to track store view for ${realStoreId}`, trackError, 'StorePage');
});
} catch (trackError) {
logger.error(`[StorePage] Error capturing store view for ${domain}`, trackError, 'StorePage');
}
const result = await controller.handle(props);

return (
<>
Expand All @@ -84,34 +35,29 @@ export default async function StorePage({ params, searchParams }: StorePageProps
</>
);
} catch (error: any) {
logger.error(`Error rendering store page ${store}${path}`, error, 'StorePage');

// Si hay HTML de error disponible, renderizarlo
if (error.html) {
return <div dangerouslySetInnerHTML={{ __html: error.html }} />;
}

if (error.type === 'STORE_NOT_FOUND' && error.statusCode === 404) {
notFound();
}

// Re-lanzar otros errores
throw error;
}
}

/**
* Genera metadata SEO para la página
*/
export async function generateMetadata({ params, searchParams }: StorePageProps): Promise<Metadata> {
const { store } = await params;
const awaitedSearchParams = await searchParams;
const path = awaitedSearchParams.path || '/';
export async function generateMetadata(props: StorePageProps): Promise<Metadata> {
const { path } = await props.searchParams;

if (isAssetPath(path)) {
if (path && isAssetPath(path)) {
return {
title: 'Asset',
description: 'Static asset',
};
}

return generateStoreMetadata(store, path, awaitedSearchParams);
const controller = new StoreMetadataController();
return controller.handle(props);
}
87 changes: 87 additions & 0 deletions app/[store]/src/_lib/constants/store.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Constantes para el módulo de tiendas
*/

/**
* Configuración de dominios
*/
export const DOMAIN_CONFIG = {
BASE_DOMAIN: 'fasttify.com',
WWW_DOMAIN: 'www.fasttify.com',
LOCALHOST: 'localhost',
} as const;

/**
* Header names para extracción de hostname
*/
export const HEADER_NAMES = {
X_ORIGINAL_HOST: 'x-original-host',
X_FORWARDED_HOST: 'x-forwarded-host',
HOST: 'host',
CF_CONNECTING_IP: 'cf-connecting-ip',
} as const;

/**
* Protocolos y separadores
*/
export const URL_CONFIG = {
PROTOCOL: 'https://',
PORT_SEPARATOR: ':',
} as const;

/**
* Tipos de errores
*/
export const ERROR_TYPES = {
STORE_NOT_FOUND: 'STORE_NOT_FOUND',
} as const;

/**
* Códigos de estado HTTP
*/
export const HTTP_STATUS = {
NOT_FOUND: 404,
} as const;

/**
* Assets comunes que los navegadores solicitan automáticamente
*/
export const COMMON_ASSETS = [
'favicon.ico',
'robots.txt',
'sitemap.xml',
'apple-touch-icon.png',
'manifest.json',
] as const;

/**
* Extensiones de archivos consideradas como assets estáticos
*/
export const ASSET_EXTENSIONS = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.webp',
'.ico',
'.css',
'.js',
'.woff',
'.woff2',
'.ttf',
'.eot',
'.otf',
'.json',
'.xml',
'.txt',
] as const;

/**
* Patrones de paths que indican assets
*/
export const ASSET_PATH_PATTERNS = {
ASSETS_FOLDER: '/assets/',
NEXT_FOLDER: '/_next/',
ICONS_FOLDER: '/icons/',
} as const;
23 changes: 23 additions & 0 deletions app/[store]/src/_lib/controllers/store-metadata-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Metadata } from 'next';
import { generateStoreMetadata as generateMetadataUtil } from '@/app/[store]/src/lib/store-page-utils';

interface StoreMetadataProps {
params: Promise<{ store: string }>;
searchParams: Promise<{ path?: string; [key: string]: string | string[] | undefined }>;
}

/**
* Controlador para generar metadata SEO de las páginas de tiendas
*/
export class StoreMetadataController {
/**
* Genera metadata SEO para la página
*/
async handle(props: StoreMetadataProps): Promise<Metadata> {
const { store } = await props.params;
const awaitedSearchParams = await props.searchParams;
const path = awaitedSearchParams.path || '/';

return generateMetadataUtil(store, path, awaitedSearchParams);
}
}
Loading