diff --git a/app/(setup)/login/components/sing-in/SignInForm.tsx b/app/(setup)/login/components/sing-in/SignInForm.tsx index a96044c7..1707553f 100644 --- a/app/(setup)/login/components/sing-in/SignInForm.tsx +++ b/app/(setup)/login/components/sing-in/SignInForm.tsx @@ -59,6 +59,7 @@ export function SignInForm({ onForgotPassword, onVerificationNeeded, redirectPat Correo electrónico { @@ -92,6 +93,7 @@ export function SignInForm({ onForgotPassword, onVerificationNeeded, redirectPat
-
+
diff --git a/app/(www)/pricing/components/PricingCard.tsx b/app/(www)/pricing/components/PricingCard.tsx index cb44cc8a..83e79e59 100644 --- a/app/(www)/pricing/components/PricingCard.tsx +++ b/app/(www)/pricing/components/PricingCard.tsx @@ -1,26 +1,7 @@ -import outputs from '@/amplify_outputs.json'; import { Button } from '@/components/ui/button'; -import { LoadingIndicator } from '@/components/ui/loading-indicator'; -import { Toast } from '@/components/ui/toasts'; -import { useAuth } from '@/context/hooks/useAuth'; -import { useIsClient } from '@/hooks/ui/useIsClient'; -import { useToast } from '@/hooks/ui/use-toasts'; -import { Amplify } from 'aws-amplify'; -import { post } from 'aws-amplify/api'; import { motion } from 'framer-motion'; import { Check } from 'lucide-react'; import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); -Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, -}); interface PricingCardProps { plan: { @@ -36,22 +17,8 @@ interface PricingCardProps { }; } -interface SubscriptionResponse { - checkoutUrl?: string; - error?: string; - details?: string; -} - export function PricingCard({ plan }: PricingCardProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - const isClient = useIsClient(); - const { user, loading } = useAuth(); - const { toasts, addToast, removeToast } = useToast(); const router = useRouter(); - - const cognitoUsername = user?.userId; - const hasActivePlan = user && user.plan ? user.plan === plan.name : false; - const formatPrice = (price: string) => { if (price === '0') return 'Gratis'; const numPrice = Number.parseInt(price, 10); @@ -63,118 +30,52 @@ export function PricingCard({ plan }: PricingCardProps) { return `$ ${formattedPrice}`; }; - const handleSubscribe = async () => { - if (!user) { - router.push('/login'); - return; - } - - setIsSubmitting(true); - try { - const restOperation = post({ - apiName: 'SubscriptionApi', - path: 'subscribe', - options: { - body: { - userId: cognitoUsername || '', - email: user.email, - name: user.nickName || '', - plan: { - polarId: plan.polarId, - }, - }, - }, - }); - - const { body } = await restOperation.response; - const response = (await body.json()) as SubscriptionResponse; - - if (response && response.checkoutUrl) { - window.location.href = response.checkoutUrl; - } else { - throw new Error('Url not found'); - } - } catch (error) { - console.error('Error processing subscription:', error); - addToast('Hubo un error al procesar tu suscripción. Por favor, inténtalo de nuevo.', 'error'); - setIsSubmitting(false); - } + const handleSubscribe = () => { + router.push('/my-store'); }; - useEffect(() => { - const handleVisibilityChange = () => { - if (!document.hidden && isSubmitting) { - setIsSubmitting(false); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }; - }, [isSubmitting]); - - if (!isClient || loading) { - return ; - } - return ( - <> - {isSubmitting && ( -
- + + {plan.popular && ( +
+
+ Más popular +
)} - - - {plan.popular && ( -
-
- Más popular -
-
- )} -
-
-

{plan.name}

-

{plan.description}

-
- {formatPrice(plan.price)} - COP al mes -

facturación mensual

-
-
- -
-

Funciones destacadas

-
    - {plan.features.map((feature) => ( -
  • - - {feature} -
  • - ))} -
+
+
+

{plan.name}

+

{plan.description}

+
+ {formatPrice(plan.price)} + COP al mes +

facturación mensual

+
- +
+

Funciones destacadas

+
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
- - - + +
+ ); } diff --git a/app/(www)/pricing/page.tsx b/app/(www)/pricing/page.tsx index a62a1e9c..1516876e 100644 --- a/app/(www)/pricing/page.tsx +++ b/app/(www)/pricing/page.tsx @@ -1,25 +1,13 @@ 'use client'; -import outputs from '@/amplify_outputs.json'; import { Footer } from '@/app/(www)/landing/components/Footer'; import { faqItems } from '@/app/(www)/pricing/components/FAQItem'; import { FAQSection } from '@/app/(www)/pricing/components/FAQSection'; import { FeatureComparison } from '@/app/(www)/pricing/components/FeatureComparison'; import { plans } from '@/app/(www)/pricing/components/plans'; import { PricingCard } from '@/app/(www)/pricing/components/PricingCard'; -import { Amplify } from 'aws-amplify'; import { motion } from 'framer-motion'; -Amplify.configure(outputs); -const existingConfig = Amplify.getConfig(); -Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, -}); - export default function PricingPage() { return ( <> diff --git a/app/store/[slug]/access_account/checkout/CheckoutModalClient.tsx b/app/store/[slug]/access_account/checkout/CheckoutModalClient.tsx new file mode 100644 index 00000000..93abad7f --- /dev/null +++ b/app/store/[slug]/access_account/checkout/CheckoutModalClient.tsx @@ -0,0 +1,81 @@ +'use client'; + +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CheckoutModal } from '@/app/store/components/checkout-modal/CheckoutModal'; +import { CheckoutModalMobile } from '@/app/store/components/checkout-modal/CheckoutModalMobile'; +import { useEffect } from 'react'; +import useAuthStore from '@/context/core/userStore'; + +/** + * Componente cliente que maneja el modal de checkout bloqueante + * Previene la navegación y aplica efectos visuales al fondo + */ +export function CheckoutModalClient({ storeId: _storeId }: { storeId: string }) { + const { logout } = useAuthStore(); + /** + * Previene la navegación del usuario mientras está en el modal de checkout + */ + useEffect(() => { + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault(); + event.returnValue = ''; + }; + + const handlePopState = () => { + // Prevenir navegación hacia atrás + window.history.pushState(null, '', window.location.href); + }; + + // Agregar el estado actual al historial para prevenir navegación + window.history.pushState(null, '', window.location.href); + + // Agregar listeners para prevenir navegación + window.addEventListener('beforeunload', handleBeforeUnload); + window.addEventListener('popstate', handlePopState); + + // Bloquear el scroll del body + document.body.style.overflow = 'hidden'; + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('popstate', handlePopState); + document.body.style.overflow = 'unset'; + }; + }, []); + + /** + * Maneja el cierre de sesión y redirige al login + */ + const handleLogout = () => { + logout(); + }; + + return ( + <> + {/* Modal para desktop */} +
+ +
+ + {/* Modal para móvil */} +
+ +
+ + ); +} diff --git a/app/store/[slug]/access_account/checkout/page.tsx b/app/store/[slug]/access_account/checkout/page.tsx new file mode 100644 index 00000000..9a1cf87b --- /dev/null +++ b/app/store/[slug]/access_account/checkout/page.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CheckoutModalClient } from '@/app/store/[slug]/access_account/checkout/CheckoutModalClient'; + +interface CheckoutPageProps { + params: Promise<{ slug: string }>; +} + +/** + * Página de checkout para usuarios con suscripción expirada que necesitan reactivar + */ +export default async function CheckoutPage({ params }: CheckoutPageProps) { + const { slug: storeId } = await params; + + // El middleware ya validó todo, solo renderizar el modal + return ; +} diff --git a/app/store/components/checkout-modal/CheckoutModal.tsx b/app/store/components/checkout-modal/CheckoutModal.tsx new file mode 100644 index 00000000..ce3f4654 --- /dev/null +++ b/app/store/components/checkout-modal/CheckoutModal.tsx @@ -0,0 +1,196 @@ +'use client'; + +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, Card, Text, BlockStack } from '@shopify/polaris'; +import { ExitIcon } from '@shopify/polaris-icons'; +import { AnimatedBackground } from '@/app/(setup)/my-store/components/AnimatedBackground'; +import { useCheckoutPayment } from './hooks/useCheckoutPayment'; + +interface CheckoutModalProps { + open: boolean; + onClose?: () => void; +} + +/** + * Modal bloqueante de checkout basado en el diseño de Polar.sh + * Este modal no se puede cerrar y bloquea toda navegación hasta completar el pago + */ +export function CheckoutModal({ open, onClose }: CheckoutModalProps) { + const { isSubmitting, handlePayment } = useCheckoutPayment(); + + if (!open) { + return null; + } + + const benefits = [ + 'Asesoría por chat, email y WhatsApp', + 'Hosting y SSL gratuitos', + 'Panel de estadísticas avanzadas', + 'Soporte prioritario 24/7', + 'Integraciones con redes sociales', + ]; + + return ( +
+ {/* Backdrop con blur */} +
+ + {/* Modal */} +
+ {/* Header con botón de logout */} +
+ +
+ + {/* Contenido principal */} +
+ {/* Panel izquierdo - Información del plan */} +
+ {/* Animated Background */} + + {/* Contenido */} +
+
+ + Vuelve al negocio por $55.000 COP/mes + + +
+
+ + Plan gratuito expirado + + + ¡Buenas noticias - guardamos tu progreso! + +
+ +
+ + Hoy - $55.000 COP/mes + + + Eso es acceso completo a todas las funciones + +
+ +
+ + Siempre - Sin compromiso, cancela cuando quieras + +
+
+ + {/* Beneficios del plan */} +
+ + Con tu plan obtienes: + +
+ {benefits.map((benefit, index) => ( +
+ + {benefit} + +
+ ))} +
+
+
+ +
+ + +
+
+
+ + {/* Panel derecho - Formulario de pago */} +
+
+ +
+ + Resumen del plan + + +
+ +
+ +
+ + Plan Royal + + + Acceso completo a todas las funciones + +
+ +
+ + $55.000 COP/mes + + + Facturación mensual + +
+
+
+
+
+
+ + {/* Botón de suscripción */} + + + + Más impuestos aplicables. Se renueva mensualmente en plan Royal $55.000/mes + +
+
+
+
+
+
+ ); +} diff --git a/app/store/components/checkout-modal/CheckoutModalMobile.tsx b/app/store/components/checkout-modal/CheckoutModalMobile.tsx new file mode 100644 index 00000000..03cca648 --- /dev/null +++ b/app/store/components/checkout-modal/CheckoutModalMobile.tsx @@ -0,0 +1,185 @@ +'use client'; + +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, Card, Text, BlockStack } from '@shopify/polaris'; +import { ExitIcon } from '@shopify/polaris-icons'; +import { useCheckoutPayment } from './hooks/useCheckoutPayment'; + +interface CheckoutModalMobileProps { + open: boolean; + onClose?: () => void; +} + +/** + * Modal móvil de checkout optimizado para pantallas pequeñas + */ +export function CheckoutModalMobile({ open, onClose }: CheckoutModalMobileProps) { + const { isSubmitting, handlePayment } = useCheckoutPayment(); + + if (!open) { + return null; + } + + const benefits = [ + 'Asesoría por chat, email y WhatsApp', + 'Hosting y SSL gratuitos', + 'Panel de estadísticas avanzadas', + 'Soporte prioritario 24/7', + 'Integraciones con redes sociales', + ]; + + return ( +
+ {/* Backdrop con blur */} +
+ + {/* Modal móvil */} +
+ {/* Panel superior - Información del plan */} +
+ {/* Fondo personalizado para móvil */} +
+ +
+ + Vuelve al negocio por $55.000 COP/mes + + +
+
+ + Plan gratuito expirado + + + ¡Buenas noticias - guardamos tu progreso! + +
+ +
+ + Hoy - $55.000 COP/mes + + + Eso es acceso completo a todas las funciones + +
+ +
+ + Siempre - Sin compromiso, cancela cuando quieras + +
+
+ + {/* Beneficios del plan */} +
+ + Con tu plan obtienes: + +
+ {benefits.map((benefit, index) => ( +
+ + {benefit} + +
+ ))} +
+
+ +
+ + +
+
+
+ + {/* Panel inferior - Formulario de pago */} +
+ {/* Botón de logout en la parte blanca */} +
+ +
+ +
+ +
+ + Resumen del plan + + +
+ +
+ +
+ + Plan Royal + + + Acceso completo a todas las funciones + +
+ +
+ + $55.000 COP/mes + + + Facturación mensual + +
+
+
+
+
+
+ + {/* Botón de suscripción */} + + + + Más impuestos aplicables. Se renueva mensualmente en plan Royal $55.000/mes + +
+
+
+
+
+ ); +} diff --git a/app/store/components/checkout-modal/hooks/useCheckoutPayment.ts b/app/store/components/checkout-modal/hooks/useCheckoutPayment.ts new file mode 100644 index 00000000..4c613592 --- /dev/null +++ b/app/store/components/checkout-modal/hooks/useCheckoutPayment.ts @@ -0,0 +1,78 @@ +'use client'; + +import { useState } from 'react'; +import { useAuth } from '@/context/hooks/useAuth'; +import { post } from 'aws-amplify/api'; +import { useToast } from '@/app/store/context/ToastContext'; + +interface SubscriptionResponse { + checkoutUrl?: string; + error?: string; + details?: string; +} + +/** + * Hook personalizado para manejar el proceso de pago en el modal de checkout + * Integra con Polar.sh para crear la sesión de pago + */ +export function useCheckoutPayment() { + const [isSubmitting, setIsSubmitting] = useState(false); + const { user } = useAuth(); + const { showToast } = useToast(); + + /** + * Maneja el proceso de suscripción y redirige a Polar.sh + */ + const handlePayment = async () => { + if (!user) { + showToast('Debes iniciar sesión para continuar', true); + return; + } + + if (!user.userId || !user.email || !user.nickName) { + showToast('Información de usuario incompleta', true); + return; + } + + setIsSubmitting(true); + + try { + // ID del plan Royal (el plan por defecto para el checkout) + const royalPlanId = 'e02f173f-1ca5-4f7b-a900-2e5c9413d8a6'; + + const restOperation = post({ + apiName: 'SubscriptionApi', + path: 'subscribe', + options: { + body: { + userId: user.userId, + email: user.email, + name: user.nickName, + plan: { + polarId: royalPlanId, + }, + }, + }, + }); + + const { body } = await restOperation.response; + const response = (await body.json()) as SubscriptionResponse; + + if (response && response.checkoutUrl) { + // Redirigir a Polar.sh para completar el pago + window.location.href = response.checkoutUrl; + } else { + throw new Error('No se recibió URL de checkout'); + } + } catch (error) { + console.error('Error procesando suscripción:', error); + showToast('Hubo un error al procesar tu suscripción. Por favor, inténtalo de nuevo.', true); + setIsSubmitting(false); + } + }; + + return { + isSubmitting, + handlePayment, + }; +} diff --git a/app/store/layout/StoreLayoutClient.tsx b/app/store/layout/StoreLayoutClient.tsx index e7b3b63d..f1f9eab1 100644 --- a/app/store/layout/StoreLayoutClient.tsx +++ b/app/store/layout/StoreLayoutClient.tsx @@ -36,6 +36,7 @@ export const StoreLayoutClient = ({ children }: { children: React.ReactNode }) = const [prefersReducedMotion, _setPrefersReducedMotion] = useState(false); const hideSidebar = pathname.includes('/editor') || pathname.includes('/profile'); + const isCheckoutPage = pathname.includes('/access_account/checkout'); return ( @@ -44,6 +45,22 @@ export const StoreLayoutClient = ({ children }: { children: React.ReactNode }) = {hideSidebar ? (
{children}
+ ) : isCheckoutPage ? ( +
+ {/* Layout completo con blur - Sidebar, TopBar y contenido */} +
+ +
+

Panel de Administración

+

+ Este contenido está bloqueado hasta que renueves tu suscripción +

+
+
+
+ {/* Modal de checkout superpuesto */} + {children} +
) : ( {children} diff --git a/context/core/userStore.ts b/context/core/userStore.ts index c9083f98..d7da036f 100644 --- a/context/core/userStore.ts +++ b/context/core/userStore.ts @@ -177,7 +177,12 @@ const useAuthStore = create((set, get) => ({ logout: async () => { try { - await signOut(); + await signOut({ + global: true, + oauth: { + redirectUrl: '/login', + }, + }); } catch (error) { console.error('Error during logout:', error); } finally { diff --git a/lib/utils/subscription-utils.ts b/lib/utils/subscription-utils.ts new file mode 100644 index 00000000..5c478602 --- /dev/null +++ b/lib/utils/subscription-utils.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Verifica si el plan del usuario está expirado o inactivo + * @param plan - Nombre del plan del usuario + * @param nextPaymentDate - Fecha del próximo pago en formato ISO string + * @returns true si el plan requiere renovación, false si está activo + */ +export function isPlanExpiredOrInactive(plan?: string, nextPaymentDate?: string): boolean { + // Planes activos que no requieren renovación + const activePlans = ['Royal', 'Majestic', 'Imperial']; + + // Si no hay plan o es plan gratuito, requiere renovación + if (!plan || plan === 'free' || !activePlans.includes(plan)) { + return true; + } + + // Si no hay fecha de próximo pago, el plan está expirado + if (!nextPaymentDate) { + return true; + } + + // Verificar si la fecha de próximo pago ya pasó + const paymentDate = new Date(nextPaymentDate); + const currentDate = new Date(); + + // Si la fecha de pago ya pasó, el plan está expirado + return paymentDate <= currentDate; +} + +/** + * Verifica si el usuario tiene un plan activo válido + * @param plan - Nombre del plan del usuario + * @param nextPaymentDate - Fecha del próximo pago en formato ISO string + * @returns true si el plan está activo, false si requiere renovación + */ +export function hasActivePlan(plan?: string, nextPaymentDate?: string): boolean { + return !isPlanExpiredOrInactive(plan, nextPaymentDate); +} + +/** + * Obtiene el estado de suscripción del usuario basado en los datos de Cognito + * @param userPlan - Plan del usuario desde Cognito + * @param subscriptionData - Datos de suscripción desde DynamoDB + * @returns objeto con el estado de la suscripción + */ +export function getSubscriptionStatus(userPlan?: string, subscriptionData?: { nextPaymentDate?: string }) { + const nextPaymentDate = subscriptionData?.nextPaymentDate; + const isExpired = isPlanExpiredOrInactive(userPlan, nextPaymentDate); + const isActive = hasActivePlan(userPlan, nextPaymentDate); + + return { + isExpired, + isActive, + plan: userPlan, + nextPaymentDate, + requiresCheckout: isExpired, + }; +} diff --git a/middleware.ts b/middleware.ts index 0ef18d28..e09eaae1 100644 --- a/middleware.ts +++ b/middleware.ts @@ -198,7 +198,7 @@ async function handleOAuthProtection( ): Promise { if (request.nextUrl.pathname === PROTECTED_ROUTES.OAUTH_CALLBACK) { if (!isValidOAuthCallback(request.nextUrl)) { - return NextResponse.redirect(new URL(PROTECTED_ROUTES.LOGIN, request.url)); + return NextResponse.redirect(new URL(PROTECTED_ROUTES.LOGIN, request.url), { status: 302 }); } } @@ -218,7 +218,7 @@ async function handleOrdersSubdomainProtection( const domainAnalysis = analyzeDomain(request); if (shouldBlockOrdersAccess(request.nextUrl.pathname, domainAnalysis.subdomain)) { - return NextResponse.redirect(new URL(PROTECTED_ROUTES.HOME, request.url)); + return NextResponse.redirect(new URL(PROTECTED_ROUTES.HOME, request.url), { status: 302 }); } return await next(); diff --git a/middlewares/auth/auth.ts b/middlewares/auth/auth.ts index 42cccd1a..d074a075 100644 --- a/middlewares/auth/auth.ts +++ b/middlewares/auth/auth.ts @@ -26,6 +26,8 @@ export interface AuthSession { payload?: { 'cognito:username'?: string; 'custom:plan'?: string; + email?: string; + nickname?: string; }; }; }; @@ -37,6 +39,15 @@ const sessionCache = new NodeCache({ useClones: false, }); +/** + * Limpia el caché de sesiones para un usuario específico + * Útil cuando se detectan problemas de autenticación + */ +export function clearUserSessionCache(request: NextRequest): void { + const cacheKey = getCacheKey(request); + sessionCache.del(cacheKey); +} + function getCacheKey(request: NextRequest): string { // Usar un hash simple de las cookies principales de autenticación const cookies = request.headers?.get('cookie') || ''; @@ -69,14 +80,20 @@ export async function getSession(request: NextRequest, response: NextResponse, f const session = await fetchAuthSession(contextSpec, { forceRefresh }); const result = session.tokens !== undefined ? session : null; - // Guardar en cache solo si la sesión es válida - if (result && result.tokens) { - sessionCache.set(cacheKey, result); + // Limpiar caché si la sesión no es válida + if (!result || !result.tokens) { + sessionCache.del(cacheKey); + return null; } + // Guardar en cache solo si la sesión es válida + sessionCache.set(cacheKey, result); + return result; } catch (error) { console.error('Error fetching user session:', error); + // Limpiar caché en caso de error + sessionCache.del(cacheKey); return null; } }, @@ -87,24 +104,31 @@ export async function handleAuthenticationMiddleware(request: NextRequest, respo const session = await getSession(request, response); if (!session) { - return NextResponse.redirect(new URL('/login', request.url)); + // Limpiar caché cuando no hay sesión válida + clearUserSessionCache(request); + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } return null; // Permitir que el middleware continúe } export async function handleAuthenticatedRedirectMiddleware(request: NextRequest, response: NextResponse) { - const session = await getSession(request, response, false); + // Siempre forzar refresh para verificar la sesión actual, especialmente importante + // cuando el usuario navega manualmente a /login + const session = await getSession(request, response, true); - if (session) { + if (session && typeof session === 'object' && 'tokens' in session && session.tokens) { + // Verificar que la sesión tiene tokens válidos antes de redirigir const lastStoreId = getLastVisitedStore(request); if (lastStoreId) { - return NextResponse.redirect(new URL(`/store/${lastStoreId}/home`, request.url)); + return NextResponse.redirect(new URL(`/store/${lastStoreId}/home`, request.url), { status: 302 }); } else { - return NextResponse.redirect(new URL('/my-store', request.url)); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); } } + // Si no hay sesión válida, limpiar caché y permitir continuar (mostrar login) + clearUserSessionCache(request); return response; } diff --git a/middlewares/ownership/collectionOwnership.ts b/middlewares/ownership/collectionOwnership.ts index d78df074..30888370 100644 --- a/middlewares/ownership/collectionOwnership.ts +++ b/middlewares/ownership/collectionOwnership.ts @@ -50,7 +50,7 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; if (!userId || typeof userId !== 'string') { - return NextResponse.redirect(new URL('/login', request.url)); + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } // Extraer información de la URL @@ -61,7 +61,7 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) if (!currentStoreId) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -87,7 +87,7 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) if (!userStoreResult.data || userStoreResult.data.length === 0) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -114,14 +114,14 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) if (!collection) { const redirectUrl = new URL(`/store/${currentStoreId}/products/collections`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } if (collection.storeId !== currentStoreId) { const redirectUrl = new URL(`/store/${currentStoreId}/products/collections`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -129,7 +129,7 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) return NextResponse.next(); } catch (_error) { const redirectUrl = new URL(`/my-store`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } diff --git a/middlewares/ownership/pagesOwnership.ts b/middlewares/ownership/pagesOwnership.ts index 3e8bff34..4fb5acdc 100644 --- a/middlewares/ownership/pagesOwnership.ts +++ b/middlewares/ownership/pagesOwnership.ts @@ -50,7 +50,7 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; if (!userId || typeof userId !== 'string') { - return NextResponse.redirect(new URL('/login', request.url)); + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } // Extraer información de la URL @@ -61,7 +61,7 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { if (!currentStoreId) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -87,7 +87,7 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { if (!userStoreResult.data || userStoreResult.data.length === 0) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -114,14 +114,14 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { if (!page) { const redirectUrl = new URL(`/store/${currentStoreId}/setup/pages`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } if (page.storeId !== currentStoreId) { const redirectUrl = new URL(`/store/${currentStoreId}/setup/pages/${pageId}`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -129,7 +129,7 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { return NextResponse.next(); } catch (_error) { const redirectUrl = new URL(`/my-store`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } diff --git a/middlewares/ownership/productOwnership.ts b/middlewares/ownership/productOwnership.ts index 72288ffa..48843429 100644 --- a/middlewares/ownership/productOwnership.ts +++ b/middlewares/ownership/productOwnership.ts @@ -50,7 +50,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; if (!userId || typeof userId !== 'string') { - return NextResponse.redirect(new URL('/login', request.url)); + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } // Extraer información de la URL @@ -61,7 +61,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { if (!currentStoreId) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -87,7 +87,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { if (!userStoreResult.data || userStoreResult.data.length === 0) { const redirectUrl = new URL('/my-store', request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -117,14 +117,14 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { if (!product) { const redirectUrl = new URL(`/store/${currentStoreId}/products`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } if (product.storeId !== currentStoreId) { const redirectUrl = new URL(`/store/${currentStoreId}/products`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } @@ -132,7 +132,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { return NextResponse.next(); } catch (_error) { const redirectUrl = new URL(`/my-store`, request.url); - const response = NextResponse.redirect(redirectUrl); + const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.headers.set('x-redirect-check', 'true'); return response; } diff --git a/middlewares/store-access/store.ts b/middlewares/store-access/store.ts index 83724d65..4b82388d 100644 --- a/middlewares/store-access/store.ts +++ b/middlewares/store-access/store.ts @@ -16,6 +16,7 @@ import { getSession, handleAuthenticationMiddleware, type AuthSession } from '@/middlewares/auth/auth'; import { cookiesClient } from '@/utils/client/AmplifyUtils'; +import { getLastVisitedStore } from '@/lib/cookies/last-store'; import { NextRequest, NextResponse } from 'next/server'; const STORE_LIMITS = { @@ -24,12 +25,6 @@ const STORE_LIMITS = { Royal: 1, }; -async function hasValidPlan(session: AuthSession) { - const userPlan = session.tokens?.idToken?.payload?.['custom:plan'] as string | undefined; - const allowedPlans = ['Royal', 'Majestic', 'Imperial']; - return userPlan && allowedPlans.includes(userPlan); -} - async function checkStoreLimit(userId: string, plan: string) { try { const { data: stores } = await cookiesClient.models.UserStore.listUserStoreByUserId({ @@ -52,6 +47,13 @@ async function checkStoreLimit(userId: string, plan: string) { } export async function handleStoreMiddleware(request: NextRequest, response: NextResponse) { + const path = request.nextUrl.pathname; + + // Excluir rutas de checkout completamente - no validar nada + if (path.includes('/access_account/checkout') || path.includes('/checkout')) { + return response; + } + // Verificar autenticación usando el middleware centralizado const authResponse = await handleAuthenticationMiddleware(request, response); if (authResponse) { @@ -63,24 +65,67 @@ export async function handleStoreMiddleware(request: NextRequest, response: Next const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; const userPlan = (session as AuthSession).tokens?.idToken?.payload?.['custom:plan']; - const hasValidSubscription = await hasValidPlan(session as AuthSession); - if (!hasValidSubscription) { - return NextResponse.redirect(new URL('/pricing', request.url)); + if (!userId) { + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); + } + + // Verificar plan de suscripción - misma lógica que storeAccess.ts + const validPlans = ['Royal', 'Majestic', 'Imperial']; + + if (!userPlan || !validPlans.includes(userPlan)) { + // Usuario con plan 'free' o sin plan - verificar si tiene suscripción en DB + try { + const { data: subscriptions } = await cookiesClient.models.UserSubscription.listUserSubscriptionByUserId({ + userId, + }); + + if (subscriptions && subscriptions.length > 0) { + // Tiene suscripción en DB pero plan 'free' - necesita reactivar + const lastStoreId = getLastVisitedStore(request); + if (lastStoreId) { + return NextResponse.redirect(new URL(`/store/${lastStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } else { + // No tiene suscripción - usuario nuevo + const lastStoreId = getLastVisitedStore(request); + if (lastStoreId) { + return NextResponse.redirect(new URL(`/store/${lastStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } + } catch (error) { + console.error('Error checking subscription:', error); + // En caso de error, redirigir a última tienda + const lastStoreId = getLastVisitedStore(request); + if (lastStoreId) { + return NextResponse.redirect(new URL(`/store/${lastStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } } - const path = request.nextUrl.pathname; const { hasStores, canCreateMore } = await checkStoreLimit(userId as string, userPlan as string); if (path === '/first-steps') { if (hasStores && !canCreateMore) { - return NextResponse.redirect(new URL('/my-store', request.url)); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); } } if (path === '/my-store') { if (!hasStores) { - return NextResponse.redirect(new URL('/first-steps', request.url)); + return NextResponse.redirect(new URL('/first-steps', request.url), { status: 302 }); } } diff --git a/middlewares/store-access/storeAccess.ts b/middlewares/store-access/storeAccess.ts index 1586ec7b..daeeb851 100644 --- a/middlewares/store-access/storeAccess.ts +++ b/middlewares/store-access/storeAccess.ts @@ -16,6 +16,7 @@ import { getSession, handleAuthenticationMiddleware, type AuthSession } from '@/middlewares/auth/auth'; import { cookiesClient } from '@/utils/client/AmplifyUtils'; +import { getLastVisitedStore } from '@/lib/cookies/last-store'; import { NextRequest, NextResponse } from 'next/server'; /** @@ -23,6 +24,9 @@ import { NextRequest, NextResponse } from 'next/server'; * Verifica que el usuario tenga acceso a la tienda solicitada y un plan de suscripción válido */ export async function handleStoreAccessMiddleware(request: NextRequest) { + // Extraer el ID de la tienda de la URL + const path = request.nextUrl.pathname; + // Verificar autenticación usando el middleware centralizado const authResponse = await handleAuthenticationMiddleware(request, NextResponse.next()); if (authResponse) { @@ -32,33 +36,108 @@ export async function handleStoreAccessMiddleware(request: NextRequest) { // Obtener la sesión del usuario (ya validada) const session = await getSession(request, NextResponse.next(), false); - // Verificar plan de suscripción válido ANTES de verificar acceso a tienda - const userPlan: string | undefined = (session as AuthSession).tokens?.idToken?.payload?.['custom:plan'] as - | string - | undefined; - const allowedPlans = ['Royal', 'Majestic', 'Imperial']; + // Obtener el ID del usuario y plan desde la sesión + const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; + const userPlan = (session as AuthSession).tokens?.idToken?.payload?.['custom:plan'] as string | undefined; - if (!userPlan || !allowedPlans.includes(userPlan)) { - return NextResponse.redirect(new URL('/pricing', request.url)); + if (!userId) { + return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } - // Obtener el ID del usuario desde la sesión - const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; + // Si es ruta de checkout, verificar que el usuario tenga el perfil correcto Y sea dueño de la tienda + if (path.includes('/access_account/checkout')) { + // Extraer el storeId de la URL + const storeIdMatch = path.match(/\/store\/([^\/]+)/); + if (!storeIdMatch || !storeIdMatch[1]) { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } - if (!userId) { - return NextResponse.redirect(new URL('/login', request.url)); + const requestedStoreId = storeIdMatch[1]; + + // Si tiene plan válido, no debe estar aquí + const validPlans = ['Royal', 'Majestic', 'Imperial']; + if (userPlan && validPlans.includes(userPlan)) { + return NextResponse.redirect(new URL(`/store/${requestedStoreId}/home`, request.url), { status: 302 }); + } + + // Verificar que la tienda pertenece al usuario (CRÍTICO para seguridad) + try { + const { data: stores } = await cookiesClient.models.UserStore.listUserStoreByUserId( + { + userId: userId as string, + }, + { + filter: { + storeId: { eq: requestedStoreId }, + }, + selectionSet: ['storeId'], + } + ); + + if (!stores || stores.length === 0) { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } catch (error) { + console.error('Error verifying store ownership for checkout:', error); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + + return NextResponse.next(); + } + + // Excluir otras rutas de checkout + if (path.includes('/checkout')) { + return NextResponse.next(); } - // Extraer el ID de la tienda de la URL - const path = request.nextUrl.pathname; const storeIdMatch = path.match(/\/store\/([^\/]+)/); if (!storeIdMatch || !storeIdMatch[1]) { - return NextResponse.redirect(new URL('/my-store', request.url)); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); } const requestedStoreId = storeIdMatch[1]; + // Verificar plan de suscripción + const validPlans = ['Royal', 'Majestic', 'Imperial']; + + if (!userPlan || !validPlans.includes(userPlan)) { + // Usuario con plan 'free' o sin plan - verificar si tiene suscripción en DB + try { + const { data: subscriptions } = await cookiesClient.models.UserSubscription.listUserSubscriptionByUserId({ + userId, + }); + + if (subscriptions && subscriptions.length > 0) { + // Tiene suscripción en DB pero plan 'free' - necesita reactivar + return NextResponse.redirect(new URL(`/store/${requestedStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + // No tiene suscripción - usuario nuevo - redirigir a última tienda + const lastStoreId = getLastVisitedStore(request); + if (lastStoreId) { + return NextResponse.redirect(new URL(`/store/${lastStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } + } catch (error) { + console.error('Error checking subscription:', error); + // En caso de error, redirigir a última tienda + const lastStoreId = getLastVisitedStore(request); + if (lastStoreId) { + return NextResponse.redirect(new URL(`/store/${lastStoreId}/access_account/checkout`, request.url), { + status: 302, + }); + } else { + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); + } + } + } + try { // Verificar si la tienda pertenece al usuario const { data: stores } = await cookiesClient.models.UserStore.listUserStoreByUserId( @@ -75,13 +154,13 @@ export async function handleStoreAccessMiddleware(request: NextRequest) { // Si la tienda no pertenece al usuario, redirigir a my-store if (!stores || stores.length === 0) { - return NextResponse.redirect(new URL('/my-store', request.url)); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); } // Si todo está bien (plan válido y tienda pertenece al usuario), permitir el acceso return NextResponse.next(); } catch (error) { console.error('Error verifying store access:', error); - return NextResponse.redirect(new URL('/my-store', request.url)); + return NextResponse.redirect(new URL('/my-store', request.url), { status: 302 }); } } diff --git a/middlewares/subscription/subscription.ts b/middlewares/subscription/subscription.ts index f29fcd4e..acc89e5d 100644 --- a/middlewares/subscription/subscription.ts +++ b/middlewares/subscription/subscription.ts @@ -33,7 +33,7 @@ export async function handleSubscriptionMiddleware(request: NextRequest, respons const allowedPlans = ['Royal', 'Majestic', 'Imperial']; if (!userPlan || !allowedPlans.includes(userPlan)) { - return NextResponse.redirect(new URL('/pricing', request.url)); + return NextResponse.redirect(new URL('/checkout', request.url), { status: 302 }); } return response; diff --git a/scripts/upload-base-template.js b/scripts/upload-base-template.js index 82cb6eec..74b59d2f 100644 --- a/scripts/upload-base-template.js +++ b/scripts/upload-base-template.js @@ -22,7 +22,7 @@ const BUCKET_NAME = process.env.BUCKET_NAME; const REGION = process.env.REGION_BUCKET || 'us-east-2'; const CLOUDFRONT_DISTRIBUTION_ID = process.env.CLOUDFRONT_DISTRIBUTION_ID; const BASE_TEMPLATE_PREFIX = 'base-templates/default/'; -const TEMPLATE_DIR = join(process.cwd(), 'template'); +const TEMPLATE_DIR = join(process.cwd(), 'packages/example-themes/fasttify/theme'); const FILTER_MODULES_DIR = join(process.cwd(), 'packages/liquid-forge/liquid/tags/filters/js'); const FILTER_MODULES_PREFIX = 'assets/'; diff --git a/test/unit/integration/auth-flow.test.ts b/test/unit/integration/auth-flow.test.ts index 6620be2f..7b68ae0f 100644 --- a/test/unit/integration/auth-flow.test.ts +++ b/test/unit/integration/auth-flow.test.ts @@ -23,6 +23,7 @@ jest.mock('node-cache', () => { return jest.fn().mockImplementation(() => ({ get: jest.fn(), set: jest.fn(), + del: jest.fn(), flushAll: jest.fn(), })); }); diff --git a/test/unit/middlewares/auth-cache.test.ts b/test/unit/middlewares/auth-cache.test.ts index af384714..820ff44c 100644 --- a/test/unit/middlewares/auth-cache.test.ts +++ b/test/unit/middlewares/auth-cache.test.ts @@ -2,6 +2,7 @@ jest.mock('node-cache', () => { return jest.fn().mockImplementation(() => ({ get: jest.fn(), set: jest.fn(), + del: jest.fn(), flushAll: jest.fn(), })); }); diff --git a/test/unit/middlewares/auth.test.ts b/test/unit/middlewares/auth.test.ts index ce7ccba3..6dbe2cdb 100644 --- a/test/unit/middlewares/auth.test.ts +++ b/test/unit/middlewares/auth.test.ts @@ -154,7 +154,7 @@ describe('Auth Middleware', () => { const result = await handleAuthenticationMiddleware(mockRequest, mockResponse); - expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', mockRequest.url)); + expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', mockRequest.url), { status: 302 }); expect(result).toBe(mockRedirectResponse); }); @@ -175,7 +175,7 @@ describe('Auth Middleware', () => { const result = await handleAuthenticationMiddleware(mockRequest, mockResponse); - expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', mockRequest.url)); + expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', mockRequest.url), { status: 302 }); expect(result).toBe(mockRedirectResponse); consoleErrorSpy.mockRestore(); @@ -211,7 +211,9 @@ describe('Auth Middleware', () => { const result = await handleAuthenticatedRedirectMiddleware(mockRequestWithCookies, mockResponse); - expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/my-store', mockRequestWithCookies.url)); + expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/my-store', mockRequestWithCookies.url), { + status: 302, + }); expect(result).toBe(mockRedirectResponse); }); @@ -243,7 +245,7 @@ describe('Auth Middleware', () => { await handleAuthenticationMiddleware(customRequest, mockResponse); - expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', customRequest.url)); + expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/login', customRequest.url), { status: 302 }); }); it('should handle authenticated redirect with custom URL', async () => { @@ -269,7 +271,7 @@ describe('Auth Middleware', () => { await handleAuthenticatedRedirectMiddleware(customRequest, mockResponse); - expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/my-store', customRequest.url)); + expect(NextResponse.redirect).toHaveBeenCalledWith(new URL('/my-store', customRequest.url), { status: 302 }); }); }); }); diff --git a/utils/client/routes.ts b/utils/client/routes.ts index 29ae4341..a5deecfc 100644 --- a/utils/client/routes.ts +++ b/utils/client/routes.ts @@ -60,6 +60,10 @@ export const routes = { main: (storeId: string) => `/store/${storeId}/themes`, edit: (storeId: string, themeId: string) => `/store/${storeId}/themes/${themeId}`, }, + + accessAccount: { + checkout: (storeId: string) => `/store/${storeId}/access_account/checkout?no_redirect=true`, + }, }, auth: {