diff --git a/README.md b/README.md
index 670ae490..a1026f7a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Fasttify - Dropshipping Ecommerce Platform
-Welcome to **Fasttify**, the ultimate SaaS solution for creating and managing personalized dropshipping stores effortlessly. Built on **AWS Amplify** with a modern **Next.js** front end, Fasttify combines scalability, performance, and a user-friendly interface.
+Welcome to **Fasttify**, the ultimate SaaS solution for creating and managing personalized dropshipping stores effortlessly. Built on **AWS Amplify Gen2** with a modern **Next.js** front end, Fasttify combines scalability, performance, and a user-friendly interface.
## Overview
@@ -12,21 +12,20 @@ Fasttify empowers users to build their ecommerce business with:
## Features
-- **Authentication**: Secure and customizable user sign-up and sign-in powered by AWS Cognito.
+- **Authentication**: Secure and customizable user sign-up and sign-in powered by AWS Cognito with Amplify Gen2's improved TypeScript support.
- **Subscriptions**: Integrated with **Mercado Pago**, enabling easy subscription management with upgrade, downgrade, and cancellation functionality.
-- **Custom Plans**: Personalize user plans with AWS Lambda and custom attributes.
-- **API and Database**: Leverages AWS AppSync (GraphQL API) and DynamoDB for fast, scalable data management.
+- **Custom Plans**: Personalize user plans with AWS Lambda and custom attributes, leveraging Gen2's enhanced type safety.
+- **API and Database**: Utilizes Amplify Gen2's improved data modeling with TypeScript for AWS AppSync (GraphQL API) and DynamoDB, providing fast, scalable data management.
- **Webhooks**: Stay synchronized with real-time notifications for subscription updates.
+- **Type Safety**: Benefit from Amplify Gen2's TypeScript-first approach for better developer experience and fewer runtime errors.
+- **Local Development**: Enhanced local development experience with Gen2's improved tooling and emulators.
## Quick Start
1. **Clone the Repository**:
```bash
-
git clone https://github.com/Stivenjs/Fasttify.git
-
-
cd fasttify
```
@@ -36,19 +35,23 @@ Fasttify empowers users to build their ecommerce business with:
npm install
```
-3. **Setup Amplify**:
+3. **Setup Amplify Gen2**:
- Initialize Amplify in your project:
```bash
- amplify init
+ npx @aws-amplify/cli@latest init
```
- Add authentication:
```bash
- amplify add auth
+ npx @aws-amplify/cli@latest add auth
+ ```
+ - Generate the TypeScript definitions:
+ ```bash
+ npx @aws-amplify/cli@latest generate
```
- - Push the changes:
+ - Deploy your backend:
```bash
- amplify push
+ npx @aws-amplify/cli@latest deploy
```
4. **Start the Development Server**:
@@ -59,6 +62,47 @@ Fasttify empowers users to build their ecommerce business with:
Your app will be live at `http://localhost:3000`.
+## Amplify Gen2 Benefits
+
+Fasttify leverages AWS Amplify Gen2 to provide:
+
+- **TypeScript-First Experience**: Improved type safety and developer experience.
+- **Simplified Resource Definition**: Define your backend resources using TypeScript.
+- **Enhanced Local Development**: Test your app locally with improved emulators.
+- **Flexible Deployment Options**: Deploy your entire stack or individual resources.
+- **Better Performance**: Optimized client libraries for faster application performance.
+- **Improved DX**: Better error messages and development workflows.
+
+## AWS Account Setup for Local Development
+
+### Prerequisites
+
+If you already have an AWS account and a locally configured profile, you only need to add the IAM role `AmplifyBackendDeployFullAccess` to your configured AWS profile.
+
+### IAM Identity Center Configuration
+
+If you don't have a configured AWS profile, follow these steps:
+
+1. **Enable IAM Identity Center**:
+
+ - Sign in to the AWS console
+ - Access the IAM Identity Center page and select "Enable"
+ - When prompted, select "Enable with AWS Organizations" and click "Continue"
+
+2. **Configure a user with Amplify permissions**:
+
+ - Open CloudShell from the AWS console
+ - Run commands to set up appropriate permissions
+
+3. **Create a permission set for Amplify**:
+ - In the IAM Identity Center navigation, select "Permission sets"
+ - Select "Create permission set"
+ - Choose "Custom permission set" and click "Next"
+ - Expand "AWS Managed Policies" and search for "amplify"
+ - Select "AmplifyBackendDeployFullAccess" and click "Next"
+ - Name the permission set "amplify-policy" and click "Next"
+ - Review and select "Create"
+
## Deploying to AWS
To deploy Fasttify to AWS:
@@ -67,7 +111,7 @@ To deploy Fasttify to AWS:
2. Set up branches for production and development.
3. Deploy directly from Amplify Console.
-Refer to the [AWS Amplify Deployment Guide](https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/#deploy-a-fullstack-app-to-aws) for detailed instructions.
+Refer to the [AWS Amplify Gen2 Deployment Guide](https://docs.amplify.aws/gen2/deploy/fullstack-app/) for detailed instructions.
## Contributing
diff --git a/amplify/.config/project-config.json b/amplify/.config/project-config.json
index c5bcaa8d..4719ec30 100644
--- a/amplify/.config/project-config.json
+++ b/amplify/.config/project-config.json
@@ -1,6 +1,6 @@
{
"whyContinueWithGen1": "Prefer not to answer",
- "projectName": "MasterDop",
+ "projectName": "fasttify",
"version": "3.1",
"frontend": "javascript",
"javascript": {
diff --git a/amplify/auth/post-confirmation/handler.ts b/amplify/auth/post-confirmation/handler.ts
index 85ac4f34..8a2865cb 100644
--- a/amplify/auth/post-confirmation/handler.ts
+++ b/amplify/auth/post-confirmation/handler.ts
@@ -51,6 +51,7 @@ export const handler: PostConfirmationTriggerHandler = async event => {
// Crear registro en la tabla UserSubscription
try {
await client.models.UserSubscription.create({
+ id: event.userName,
userId: event.userName,
subscriptionId: `trial-${event.userName}-${Date.now()}`, // ID único para la suscripción de prueba
planName: 'Royal',
diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts
index f13f5bb7..70aeac2d 100644
--- a/amplify/data/resource.ts
+++ b/amplify/data/resource.ts
@@ -71,6 +71,7 @@ const schema = a
UserSubscription: a
.model({
+ id: a.id().required(),
userId: a.string().required(), // Llave primaria (external_reference)
subscriptionId: a.string().required(), // Id de la suscripción
planName: a.string().required(), // Nombre del plan (reason)
@@ -80,6 +81,7 @@ const schema = a
planPrice: a.float(), // Precio del plan
lastFourDigits: a.integer(), // Últimos 4 dígitos de la tarjeta
})
+ .identifier(['id'])
.authorization(allow => [
allow.ownerDefinedIn('userId').to(['read', 'update', 'delete']),
allow.authenticated().to(['create']),
diff --git a/amplify/functions/planScheduler/handler.ts b/amplify/functions/planScheduler/handler.ts
index ea6bf8cb..f77828fb 100644
--- a/amplify/functions/planScheduler/handler.ts
+++ b/amplify/functions/planScheduler/handler.ts
@@ -21,6 +21,7 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
const now = new Date()
// 2. Consultar DynamoDB para obtener las suscripciones pendientes con un plan asignado
+ // Modificado para solo procesar suscripciones que realmente han expirado o tienen un cambio de plan programado
const pendingSubscriptionsResponse = await clientSchema.models.UserSubscription.list({
filter: {
pendingPlan: { attributeExists: true },
@@ -29,24 +30,47 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
})
const pendingSubscriptions = pendingSubscriptionsResponse.data || []
+ console.log(`Found ${pendingSubscriptions.length} subscriptions to process`)
// 3. Iterar sobre cada registro pendiente
for (const subscription of pendingSubscriptions) {
const userId = subscription.userId
if (!userId) {
- console.warn('⚠️ Suscripción sin userId, omitiendo...')
+ console.warn('Subscription without userId, skipping')
continue
}
// Leer el valor del plan pendiente desde el registro
const newPlan = subscription.pendingPlan
if (!newPlan) {
- console.warn(
- `⚠️ La suscripción de ${userId} no tiene un plan pendiente válido, omitiendo...`
+ console.warn(`The subscription for ${userId} does not have a valid pending plan, skipping`)
+ continue
+ }
+
+ // Verificar si la suscripción realmente ha expirado o es un cambio de plan programado
+ const nextPaymentDate = subscription.nextPaymentDate
+ ? new Date(subscription.nextPaymentDate)
+ : null
+ const pendingStartDate = subscription.pendingStartDate
+ ? new Date(subscription.pendingStartDate)
+ : null
+
+ // Solo procesar si:
+ // 1. No hay fecha de próximo pago (suscripción expirada)
+ // 2. La fecha de próximo pago ya pasó (suscripción expirada)
+ // 3. La fecha de inicio pendiente está definida y ya pasó (cambio de plan programado)
+ const shouldProcess =
+ !nextPaymentDate || nextPaymentDate <= now || (pendingStartDate && pendingStartDate <= now)
+
+ if (!shouldProcess) {
+ console.log(
+ `Skipping subscription for ${userId}: not expired yet and no pending plan change due`
)
continue
}
+ console.log(`Processing subscription for ${userId}: changing plan to ${newPlan}`)
+
try {
// 3.1. Actualizar el atributo en Cognito para asignar el plan pendiente
const updateCommand = new AdminUpdateUserAttributesCommand({
@@ -56,7 +80,7 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
})
await cognitoClient.send(updateCommand)
} catch (cognitoError) {
- console.error(`❌ Error actualizando usuario ${userId} en Cognito:`, cognitoError)
+ console.error(`Error updating user ${userId} in Cognito:`, cognitoError)
continue
}
@@ -73,13 +97,10 @@ export const handler: EventBridgeHandler<'Scheduled Event', null, void> = async
lastFourDigits: null,
})
} catch (dbError) {
- console.error(
- `❌ Error actualizando suscripción de usuario ${userId} en DynamoDB:`,
- dbError
- )
+ console.error(`Error updating user subscription ${userId} in DynamoDB:`, dbError)
}
}
} catch (error) {
- console.error('❌ Error en la Lambda programada:', error)
+ console.error('Error in scheduled Lambda:', error)
}
}
diff --git a/app/(with-navbar)/account-settings/components/AccountSettings.tsx b/app/(with-navbar)/account-settings/components/AccountSettings.tsx
index 69f8ee91..bb9eb424 100644
--- a/app/(with-navbar)/account-settings/components/AccountSettings.tsx
+++ b/app/(with-navbar)/account-settings/components/AccountSettings.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react'
-import { Pencil, BadgeCheck, LogOut } from 'lucide-react'
+import { Pencil, BadgeCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { EditProfileDialog } from '@/app/(with-navbar)/account-settings/components/EditProfileDialog'
import {
@@ -158,9 +158,6 @@ export function AccountSettings() {
-
- Desconectar
-
diff --git a/app/(with-navbar)/account-settings/components/CancellationDialog.tsx b/app/(with-navbar)/account-settings/components/CancellationDialog.tsx
index 01ece90a..c0490f0c 100644
--- a/app/(with-navbar)/account-settings/components/CancellationDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/CancellationDialog.tsx
@@ -42,7 +42,7 @@ export function CancellationDialog({
await onCancel()
setIsCancelled(true)
} catch (error) {
- console.error('Error al cancelar la suscripción:', error)
+ console.error('Error unsubscribing:', error)
} finally {
setIsSubmitting(false)
}
diff --git a/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx b/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
index bdad3374..b9191c22 100644
--- a/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
-import { Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import {
Dialog,
DialogContent,
@@ -44,7 +44,6 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
})
const {
- register: registerVerification,
handleSubmit: handleSubmitVerification,
formState: { errors: verificationErrors },
reset: resetVerification,
@@ -149,7 +148,7 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
{loading ? (
-
+
Procesando...
) : (
@@ -185,7 +184,7 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
>
{loading ? (
-
+
Procesando...
) : (
diff --git a/app/(with-navbar)/account-settings/components/ChangePasswordDialog.tsx b/app/(with-navbar)/account-settings/components/ChangePasswordDialog.tsx
index edf46a09..5afe3419 100644
--- a/app/(with-navbar)/account-settings/components/ChangePasswordDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/ChangePasswordDialog.tsx
@@ -1,8 +1,8 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
-import * as z from 'zod'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { passwordSchema, PasswordFormValues } from '@/lib/schemas/password-change'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
@@ -14,26 +14,8 @@ import {
FormMessage,
} from '@/components/ui/form'
import usePasswordManagement from '@/app/(with-navbar)/account-settings/hooks/usePasswordManagement'
-import { Eye, EyeOff, Loader2 } from 'lucide-react'
-
-const passwordSchema = z
- .object({
- oldPassword: z.string().min(1, 'La contraseña actual es requerida'),
- newPassword: z
- .string()
- .min(8, 'La nueva contraseña debe tener al menos 8 caracteres')
- .regex(
- /[!@#$%^&*()\-_=+{};:,<.>]/,
- 'La contraseña debe contener al menos una letra mayúscula, una minúscula, un número y un carácter especial'
- ),
- confirmPassword: z.string().min(1, 'Confirma tu nueva contraseña'),
- })
- .refine(data => data.newPassword === data.confirmPassword, {
- message: 'Las contraseñas no coinciden',
- path: ['confirmPassword'],
- })
-
-type PasswordFormValues = z.infer
+import { Eye, EyeOff } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
export function ChangePasswordDialog({
open,
@@ -159,7 +141,7 @@ export function ChangePasswordDialog({
>
{loading ? (
-
+
Actualizando...
) : (
diff --git a/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx b/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
index b1d293e2..34fb2e45 100644
--- a/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
@@ -15,18 +15,12 @@ import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import { useEffect } from 'react'
import { useUserAttributes } from '@/app/(with-navbar)/account-settings/hooks/useUserAttributes'
-import { Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { useToast } from '@/hooks/custom-toast/use-toast'
import { Toast } from '@/components/ui/toasts'
+import { formSchema } from '@/lib/schemas/email-change'
import useAuthStore from '@/zustand-states/userStore'
-const formSchema = z.object({
- firstName: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'),
- lastName: z.string().min(2, 'El apellido debe tener al menos 2 caracteres'),
- phone: z.string().min(10, 'El teléfono debe tener al menos 10 caracteres'),
- bio: z.string(),
-})
-
interface EditProfileDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -35,7 +29,7 @@ interface EditProfileDialogProps {
export function EditProfileDialog({ open, onOpenChange }: EditProfileDialogProps) {
const { toasts, addToast, removeToast } = useToast()
const { user } = useAuthStore()
- const { updateAttributes, loading, error, nextStep } = useUserAttributes()
+ const { updateAttributes, loading, nextStep } = useUserAttributes()
const fullName = user?.nickName
const nameParts = fullName ? fullName.split(' ') : []
@@ -155,7 +149,7 @@ export function EditProfileDialog({ open, onOpenChange }: EditProfileDialogProps
>
{loading ? (
-
+
Guardando
) : (
diff --git a/app/(with-navbar)/account-settings/components/PaymentSettings.tsx b/app/(with-navbar)/account-settings/components/PaymentSettings.tsx
index c8dfb816..ed14bcdf 100644
--- a/app/(with-navbar)/account-settings/components/PaymentSettings.tsx
+++ b/app/(with-navbar)/account-settings/components/PaymentSettings.tsx
@@ -1,43 +1,28 @@
-import { useEffect, useState } from 'react'
+import { Suspense, useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { useSubscriptionStore } from '@/zustand-states/useSubscriptionStore'
import { post } from 'aws-amplify/api'
import { SubscriptionCard } from '@/app/(with-navbar)/account-settings/components/SubscriptionCard'
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { CancellationDialog } from '@/app/(with-navbar)/account-settings/components/CancellationDialog'
+import { Loader } from '@/components/ui/loader'
import useUserStore from '@/zustand-states/userStore'
import Link from 'next/link'
-export function PaymentSettings() {
- const { subscription, loading, error, fetchSubscription, setCognitoUsername } =
- useSubscriptionStore()
- const { user } = useUserStore()
- const [cachedSubscription, setCachedSubscription] = useState(subscription)
+function SubscriptionLoader() {
+ const { subscriptionResource } = useSubscriptionStore()
+ const subscription = subscriptionResource.read()
const [isSubmitting, setIsSubmitting] = useState(false)
+ const { user } = useUserStore()
const cognitoUsername = user?.cognitoUsername
- useEffect(() => {
- if (cognitoUsername) {
- setCognitoUsername(cognitoUsername)
- fetchSubscription()
- }
- }, [cognitoUsername, setCognitoUsername, fetchSubscription])
-
- useEffect(() => {
- if (!loading && subscription) {
- setCachedSubscription(subscription)
- }
- }, [loading, subscription])
-
- const currentSubscription = loading ? cachedSubscription : subscription
-
const handleCancel = async () => {
if (!cognitoUsername) {
- console.error('No hay usuario autenticado.')
+ console.error('There is no authenticated user.')
return
}
- if (!currentSubscription || !currentSubscription.subscriptionId) {
- console.error('No se encontró una suscripción activa.')
+ if (!subscription || !subscription.subscriptionId) {
+ console.error('No active subscription found.')
return
}
setIsSubmitting(true)
@@ -48,12 +33,12 @@ export function PaymentSettings() {
options: {
body: {
user_id: cognitoUsername,
- preapproval_id: currentSubscription.subscriptionId,
+ preapproval_id: subscription.subscriptionId,
},
},
})
} catch (error) {
- console.error('Error al cancelar la suscripción:', error)
+ console.error('Error unsubscribing:', error)
} finally {
setIsSubmitting(false)
}
@@ -71,7 +56,7 @@ export function PaymentSettings() {
subscriptionId: string
}) => {
if (!cognitoUsername) {
- console.error('No hay usuario autenticado.')
+ console.error('There is no authenticated user.')
return
}
@@ -95,15 +80,84 @@ export function PaymentSettings() {
if (responseUrl && responseUrl.confirmationUrl) {
window.location.href = responseUrl.confirmationUrl
} else {
- console.warn('No se recibió URL de confirmación.')
+ console.warn('No confirmation URL received.')
}
} catch (error) {
- console.error('Error al actualizar el plan:', error)
+ console.error('Error updating plan:', error)
} finally {
setIsSubmitting(false)
}
}
+ if (!subscription || !subscription.nextPaymentDate) {
+ return (
+
+
+ No tienes una suscripción activa
+
+ Explora nuestros planes y elige el que mejor se adapte a tus necesidades.
+
+
+
+
+ Ver planes disponibles
+
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+// Componente de fallback para errores
+function ErrorFallback() {
+ return (
+
+
+ Error al cargar la suscripción
+ Por favor, intenta de nuevo más tarde.
+
+
+ )
+}
+
+// Componente principal
+export function PaymentSettings() {
+ const { setCognitoUsername } = useSubscriptionStore()
+ const { user } = useUserStore()
+
+ // Usar useEffect para inicializar el recurso de suscripción
+ useEffect(() => {
+ if (user?.cognitoUsername) {
+ setCognitoUsername(user.cognitoUsername)
+ }
+ }, [user?.cognitoUsername, setCognitoUsername])
+
return (
@@ -111,58 +165,21 @@ export function PaymentSettings() {
Gestiona tu plan de suscripción y método de pago
- {loading ? (
-
-
- Cargando información de suscripción...
-
-
- ) : error ? (
-
-
- Error al cargar la suscripción
- Por favor, intenta de nuevo más tarde.
-
-
- ) : !currentSubscription || !currentSubscription.nextPaymentDate ? (
-
-
- No tienes una suscripción activa
-
- Explora nuestros planes y elige el que mejor se adapte a tus necesidades.
-
-
-
-
- Ver planes disponibles
-
-
-
- ) : (
- <>
-
-
-
- >
- )}
+
+
+
+ }
+ >
+
+
)
}
diff --git a/app/(with-navbar)/account-settings/components/SideBar.tsx b/app/(with-navbar)/account-settings/components/SideBar.tsx
index 4ac1cc69..39d2740f 100644
--- a/app/(with-navbar)/account-settings/components/SideBar.tsx
+++ b/app/(with-navbar)/account-settings/components/SideBar.tsx
@@ -7,9 +7,16 @@ import { routes } from '@/utils/routes'
interface SidebarProps {
currentView: string
onViewChange: (view: string) => void
+ hideSessionsOption?: boolean
+ isUserLoading?: boolean
}
-export function Sidebar({ currentView, onViewChange }: SidebarProps) {
+export function Sidebar({
+ currentView,
+ onViewChange,
+ hideSessionsOption = false,
+ isUserLoading = false,
+}: SidebarProps) {
const router = useRouter()
const handleViewChange = (view: string): void => {
@@ -56,18 +63,20 @@ export function Sidebar({ currentView, onViewChange }: SidebarProps) {
Pagos
- handleViewChange('sesiones')}
- className={cn(
- buttonVariants({
- variant: currentView === 'sesiones' ? 'outline' : 'ghost',
- }),
- 'justify-start gap-2 w-full'
- )}
- >
-
- Sesiones Activas
-
+ {!isUserLoading && !hideSessionsOption && (
+ handleViewChange('sesiones')}
+ className={cn(
+ buttonVariants({
+ variant: currentView === 'sesiones' ? 'outline' : 'ghost',
+ }),
+ 'justify-start gap-2 w-full'
+ )}
+ >
+
+ Sesiones Activas
+
+ )}
diff --git a/app/(with-navbar)/account-settings/page.tsx b/app/(with-navbar)/account-settings/page.tsx
index 81078cd7..d25c75ed 100644
--- a/app/(with-navbar)/account-settings/page.tsx
+++ b/app/(with-navbar)/account-settings/page.tsx
@@ -5,9 +5,11 @@ import { AccountSettings } from '@/app/(with-navbar)/account-settings/components
import { PaymentSettings } from '@/app/(with-navbar)/account-settings/components/PaymentSettings'
import { ActiveSessions } from '@/app/(with-navbar)/account-settings/components/ActiveSessions'
import { useState, useEffect, Suspense } from 'react'
+import { Loader } from '@/components/ui/loader'
import { Amplify } from 'aws-amplify'
-import outputs from '@/amplify_outputs.json'
import { useSearchParams } from 'next/navigation'
+import useUserStore from '@/zustand-states/userStore'
+import outputs from '@/amplify_outputs.json'
Amplify.configure(outputs)
const existingConfig = Amplify.getConfig()
@@ -24,13 +26,22 @@ function AccountSettingsContent() {
const searchParams = useSearchParams()
const sectionParam = searchParams.get('section')
const [currentView, setCurrentView] = useState(sectionParam || 'cuenta')
+ const { user, loading } = useUserStore()
+
+ const isGoogleUser = user?.identities?.some(
+ identity => identity.providerType === 'Google' || identity.providerName === 'Google'
+ )
useEffect(() => {
// Update view when URL parameter changes
if (sectionParam && ['cuenta', 'pagos', 'sesiones'].includes(sectionParam)) {
- setCurrentView(sectionParam)
+ if (isGoogleUser && sectionParam === 'sesiones') {
+ setCurrentView('cuenta')
+ } else {
+ setCurrentView(sectionParam)
+ }
}
- }, [sectionParam])
+ }, [sectionParam, isGoogleUser])
useEffect(() => {
document.title = 'Mi Perfil • Fasttify'
@@ -38,11 +49,16 @@ function AccountSettingsContent() {
return (
-
+
{currentView === 'cuenta' &&
}
{currentView === 'pagos' &&
}
- {currentView === 'sesiones' &&
}
+ {!isGoogleUser && currentView === 'sesiones' &&
}
)
@@ -53,9 +69,13 @@ export default function AccountSettingsPage() {
+
}
>
diff --git a/app/(with-navbar)/landing/components/DocsLanding.tsx b/app/(with-navbar)/landing/components/DocsLanding.tsx
index 218b939c..2d79ce81 100644
--- a/app/(with-navbar)/landing/components/DocsLanding.tsx
+++ b/app/(with-navbar)/landing/components/DocsLanding.tsx
@@ -3,7 +3,7 @@ import { Platform } from '@/app/(with-navbar)/landing/components/Platform'
import { Footer } from '@/app/(with-navbar)/landing/components/Footer'
import { StepGuide } from '@/app/(with-navbar)/landing/components/StepGuide'
import { FirstView } from '@/app/(with-navbar)/landing/components/FirstView'
-import { AboutUs } from './AboutUs'
+import { AboutUs } from '@/app/(with-navbar)/landing/components/AboutUs'
import { FashionSlider } from '@/app/(with-navbar)/landing/components/FashionSlider'
import { Feature } from '@/app/(with-navbar)/landing/components/Feature'
import { Testimonials } from '@/app/(with-navbar)/landing/components/Testimonials'
diff --git a/app/(with-navbar)/landing/components/NavBar.tsx b/app/(with-navbar)/landing/components/NavBar.tsx
index 9e6e0f66..22d80287 100644
--- a/app/(with-navbar)/landing/components/NavBar.tsx
+++ b/app/(with-navbar)/landing/components/NavBar.tsx
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
-import { Menu, ChevronDown, X } from 'lucide-react'
+import { Menu, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Amplify } from 'aws-amplify'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
diff --git a/app/(with-navbar)/page.tsx b/app/(with-navbar)/page.tsx
index 6100636a..a7196006 100644
--- a/app/(with-navbar)/page.tsx
+++ b/app/(with-navbar)/page.tsx
@@ -3,11 +3,10 @@
import { useEffect } from 'react'
import { getCurrentUser, fetchUserAttributes } from 'aws-amplify/auth'
import { ConsoleLogger, Hub } from 'aws-amplify/utils'
-import DocsLanding from '@/app/(with-navbar)/landing/components/DocsLanding'
+import { Amplify } from 'aws-amplify'
import 'aws-amplify/auth/enable-oauth-listener'
-
+import DocsLanding from '@/app/(with-navbar)/landing/components/DocsLanding'
import outputs from '@/amplify_outputs.json'
-import { Amplify } from 'aws-amplify'
Amplify.configure(outputs)
const existingConfig = Amplify.getConfig()
@@ -31,15 +30,15 @@ export default function Home() {
const userAttributes = await fetchUserAttributes()
logger.log({ user, userAttributes })
} catch (error) {
- logger.error('Error al obtener la sesión del usuario:', error)
+ logger.error('Error getting user session:', error)
}
break
case 'signInWithRedirect_failure':
- logger.error('Error en el inicio de sesión con redirección:', payload.data)
+ logger.error('Login failed with redirect:', payload.data)
break
case 'customOAuthState':
const state = payload.data
- logger.log('Estado personalizado:', state)
+ logger.log('Custom status:', state)
break
}
})
diff --git a/app/(without-navbar)/login/components/forgot-password/ForgotPasswordForm.tsx b/app/(without-navbar)/login/components/forgot-password/ForgotPasswordForm.tsx
index 3adad5db..fa3613ba 100644
--- a/app/(without-navbar)/login/components/forgot-password/ForgotPasswordForm.tsx
+++ b/app/(without-navbar)/login/components/forgot-password/ForgotPasswordForm.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
+import { Loader } from '@/components/ui/loader'
import { resetPassword, confirmResetPassword } from 'aws-amplify/auth'
import { Button } from '@/components/ui/button'
import {
@@ -241,7 +242,14 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
className="w-full bg-black text-white hover:bg-black/90"
disabled={isLoading}
>
- {isLoading ? 'Confirmando...' : 'Confirmar nueva contraseña'}
+ {isLoading ? (
+ <>
+
+ Confirmando...
+ >
+ ) : (
+ 'Confirmar nueva contraseña'
+ )}
{isLoading ? (
<>
-
+
Iniciando sesión...
>
) : (
diff --git a/app/(without-navbar)/login/components/sing-up/SignUpForm.tsx b/app/(without-navbar)/login/components/sing-up/SignUpForm.tsx
index a432fc78..1e6557e1 100644
--- a/app/(without-navbar)/login/components/sing-up/SignUpForm.tsx
+++ b/app/(without-navbar)/login/components/sing-up/SignUpForm.tsx
@@ -3,7 +3,8 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
-import { Eye, EyeOff, Loader2 } from 'lucide-react'
+import { Eye, EyeOff } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { handleSignUp } from '@/app/(without-navbar)/login/hooks/signUp'
import { Button } from '@/components/ui/button'
import {
@@ -168,7 +169,7 @@ export function SignUpForm({ onVerificationNeeded }: SignUpFormProps) {
>
{isSubmitted ? (
<>
- Creando cuenta
+ Creando cuenta
>
) : (
'Crear cuenta'
diff --git a/app/(without-navbar)/login/components/verification-form/VerificationForm.tsx b/app/(without-navbar)/login/components/verification-form/VerificationForm.tsx
index 357c7841..d3ff3616 100644
--- a/app/(without-navbar)/login/components/verification-form/VerificationForm.tsx
+++ b/app/(without-navbar)/login/components/verification-form/VerificationForm.tsx
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
-import { Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { handleConfirmSignUp } from '@/app/(without-navbar)/login/hooks/signUp'
@@ -123,7 +123,7 @@ export function VerificationForm({ email, password, onBack }: VerificationFormPr
>
{isSubmitted ? (
<>
- Verificando código
+ Verificando código
>
) : (
'Verificar código'
diff --git a/app/store/components/ai-chat/hooks/useChat.ts b/app/store/components/ai-chat/hooks/useChat.ts
index e68b68bd..e868b571 100644
--- a/app/store/components/ai-chat/hooks/useChat.ts
+++ b/app/store/components/ai-chat/hooks/useChat.ts
@@ -58,16 +58,16 @@ export function useChat() {
})
if (errors) {
- throw new Error(errors[0]?.message || 'Error en la generación')
+ throw new Error(errors[0]?.message || 'Generation error')
} else if (data) {
// Añadir la respuesta del asistente a los mensajes
setMessages(prev => [...prev, { content: data, role: 'assistant' }])
} else {
- throw new Error('No se recibió respuesta del asistente')
+ throw new Error('No response was received from the assistant')
}
} catch (err: any) {
- console.error('Error en chat:', err)
- setError(new Error(err.message || 'Error desconocido'))
+ console.error('Error in chat:', err)
+ setError(new Error(err.message || 'Unknown error'))
// Añadir mensaje de error como respuesta del asistente
setMessages(prev => [
...prev,
diff --git a/app/store/components/app-integration/ConnectModal.tsx b/app/store/components/app-integration/ConnectModal.tsx
index 39173990..e3388f8f 100644
--- a/app/store/components/app-integration/ConnectModal.tsx
+++ b/app/store/components/app-integration/ConnectModal.tsx
@@ -68,7 +68,6 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
} else if (step === 2 && option === 'existing') {
setStatus('loading')
- // Validamos la API Key (aquí podrías hacer una validación real con la API de Master Shop)
if (apiKey.length < 5) {
setStatus('error')
setErrorMessage(
@@ -77,10 +76,8 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
return
}
- // Si tenemos el ID de la tienda, guardamos la API Key
if (currentStore?.id) {
try {
- // Encriptamos la API Key antes de guardarla
const encryptedKey = await encryptApiKey(
apiKey,
'mastershop',
@@ -89,7 +86,7 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
)
if (!encryptedKey) {
- console.error('Error al encriptar la API Key de Master Shop')
+ console.error('Error encrypting the Master Shop API Key')
setStatus('error')
setErrorMessage('No se pudo configurar la integración. Por favor intenta nuevamente.')
return
@@ -115,11 +112,9 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
setErrorMessage(
'Ocurrió un error al guardar la configuración. Por favor intenta nuevamente.'
)
- console.error('Error al guardar API Key:', error)
+ console.error('Error saving API Key:', error)
}
} else {
- // Si no tenemos el ID de la tienda, simulamos éxito (para desarrollo)
- // En producción, deberías mostrar un error
setTimeout(() => {
setStatus('success')
setStep(3)
@@ -140,7 +135,6 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
const handleOpenChange = (open: boolean) => {
if (!open) {
- // Reset state when modal is closed
setTimeout(() => {
setStep(1)
setOption(null)
@@ -156,9 +150,6 @@ export function ConnectModal({ open, onOpenChange }: ConnectModalProps) {
window.open('https://app.mastershop.com/login', '_blank')
setStatus('loading')
- // Aquí deberías implementar un mecanismo para verificar cuando el usuario
- // ha completado el registro en Master Shop y ha obtenido una API Key
- // Por ahora, simulamos el proceso
setTimeout(() => {
setStatus('success')
setStep(3)
diff --git a/app/store/components/domains/ChangeDomainDialog.tsx b/app/store/components/domains/ChangeDomainDialog.tsx
index e6d9ab85..2b594410 100644
--- a/app/store/components/domains/ChangeDomainDialog.tsx
+++ b/app/store/components/domains/ChangeDomainDialog.tsx
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
-import { Check, Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
+import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import {
Dialog,
@@ -132,7 +133,7 @@ export function ChangeDomainDialog({
{domainName && (
{isChecking ? (
-
+
) : exists ? null : hasBeenValidated ? (
) : null}
@@ -168,7 +169,7 @@ export function ChangeDomainDialog({
>
{isUpdating ? (
<>
-
+
Guardando...
>
) : (
diff --git a/app/store/components/domains/EditStoreProfileDialog.tsx b/app/store/components/domains/EditStoreProfileDialog.tsx
index aedf75c5..adb649f4 100644
--- a/app/store/components/domains/EditStoreProfileDialog.tsx
+++ b/app/store/components/domains/EditStoreProfileDialog.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { Loader } from '@/components/ui/loader'
import Link from 'next/link'
import { useUserStoreData } from '@/app/(without-navbar)/first-steps/hooks/useUserStoreData'
import { useStoreNameValidator } from '@/app/(without-navbar)/first-steps/hooks/useStoreNameValidator'
@@ -223,7 +224,14 @@ export function EditStoreProfileDialog({
className="bg-[#2a2a2a] h-9 px-4 text-sm font-medium text-white py-2 rounded-md hover:bg-[#3a3a3a] transition-colors"
disabled={isSubmitDisabled}
>
- {isUpdating || form.formState.isSubmitting ? 'Guardando...' : 'Guardar'}
+ {isUpdating || form.formState.isSubmitting ? (
+ <>
+
+ Guardando...
+ >
+ ) : (
+ 'Guardar cambios'
+ )}
diff --git a/app/store/components/domains/utils/storeProfileUtils.ts b/app/store/components/domains/utils/storeProfileUtils.ts
index 64b58898..11827f77 100644
--- a/app/store/components/domains/utils/storeProfileUtils.ts
+++ b/app/store/components/domains/utils/storeProfileUtils.ts
@@ -32,13 +32,12 @@ export const createStoreNameValidator = (
return debounce(async (name: string) => {
if (name !== originalStoreName) {
setNameChanged(true)
- // Solo verificamos con la API si tiene al menos 3 caracteres
+
if (name.length >= 3) {
await checkStoreName(name)
- // Luego de la validación, si "exists" es false, se considera válido
+
setIsStoreNameValid(!exists)
} else {
- // Mantener inválido si es muy corto
setIsStoreNameValid(false)
}
} else if (name === originalStoreName) {
@@ -128,7 +127,7 @@ export const handleStoreProfileSubmit = async (
return false
}
} catch (error) {
- console.error('Error al actualizar la información de la tienda:', error)
+ console.error('Error updating store information:', error)
toast.error('Ocurrió un error al actualizar la información')
return false
}
diff --git a/app/store/components/images-selector/image-selector-modal.tsx b/app/store/components/images-selector/image-selector-modal.tsx
index dba3cb64..b40aa1d7 100644
--- a/app/store/components/images-selector/image-selector-modal.tsx
+++ b/app/store/components/images-selector/image-selector-modal.tsx
@@ -1,5 +1,6 @@
import { useState, useRef, useCallback } from 'react'
-import { Search, Grid, List, Upload, Trash2, Loader2 } from 'lucide-react'
+import { Search, Grid, List, Upload, Trash2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -82,7 +83,7 @@ export default function ImageSelectorModal({
} finally {
setIsUploading(false)
setUploadPreview(null)
- // Limpiar el input de archivo
+
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
@@ -206,7 +207,7 @@ export default function ImageSelectorModal({
/>
-
+
Subiendo imagen...
@@ -215,7 +216,7 @@ export default function ImageSelectorModal({
{/* Loading state */}
{loading && (
-
+
Cargando imágenes...
)}
diff --git a/app/store/components/payments/ApiKeyModal.tsx b/app/store/components/payments/ApiKeyModal.tsx
index 701f3f34..3d4a3dc0 100644
--- a/app/store/components/payments/ApiKeyModal.tsx
+++ b/app/store/components/payments/ApiKeyModal.tsx
@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
-import { z } from 'zod'
import { Eye, EyeOff, Info } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
@@ -25,6 +24,8 @@ import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
+import { createApiKeySchema, PaymentGatewayType, PAYMENT_GATEWAYS } from '@/lib/schemas/api-keys'
+
import { Amplify } from 'aws-amplify'
import outputs from '@/amplify_outputs.json'
@@ -38,75 +39,6 @@ Amplify.configure({
},
})
-const PAYMENT_GATEWAYS = {
- mercadoPago: {
- name: 'Mercado Pago',
- transactionFee: 3.99,
- publicKeyPrefix: 'APP_USR-',
- privateKeyPrefix: 'TEST-',
- publicKeyPattern: /^APP_USR-[a-zA-Z0-9-]+$/,
- privateKeyPattern: /^TEST-[a-zA-Z0-9-]+$/,
- publicKeyPlaceholder: 'APP_USR-1234567890123456',
- privateKeyPlaceholder: 'Kw4aC0rZVgLZQn209NbEKPuXLzBD28Zx',
- color: 'bg-blue-100 text-blue-800 border-blue-200',
- description: 'Popular en América Latina con amplio soporte regional.',
- publicKeyLabel: 'Access Token',
- privateKeyLabel: 'Clave Secreta',
- },
- wompi: {
- name: 'Wompi',
- transactionFee: 2.9,
- publicKeyPrefix: 'pub_',
- privateKeyPrefix: 'prv_',
- publicKeyPattern: /^(pub_test|pub_prod|prod)_[a-zA-Z0-9]{16,}$/,
- privateKeyPattern: /^[a-zA-Z0-9_-]+$/,
- publicKeyPlaceholder: 'pub_prod_1234567890abcdef',
- privateKeyPlaceholder: 'prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqs',
- color: 'bg-purple-100 text-purple-800 border-purple-200',
- description: 'Tarifas más bajas con fuerte soporte en Colombia y otras regiones.',
- publicKeyLabel: 'Llave Pública',
- privateKeyLabel: 'Firma (Signature)',
- },
-}
-
-type PaymentGatewayType = keyof typeof PAYMENT_GATEWAYS
-
-const createApiKeySchema = (gateway: PaymentGatewayType) => {
- if (gateway === 'wompi') {
- return z.object({
- publicKey: z
- .string()
- .min(16, { message: 'La llave pública debe tener al menos 16 caracteres' })
- .regex(PAYMENT_GATEWAYS.wompi.publicKeyPattern, {
- message: 'Formato de llave pública inválido. Debe comenzar con pub_ o prod_',
- }),
- privateKey: z
- .string()
- .min(32, { message: 'La firma debe tener al menos 32 caracteres' })
- .max(128, { message: 'La firma no puede exceder 128 caracteres' })
- .regex(PAYMENT_GATEWAYS.wompi.privateKeyPattern, {
- message: 'La firma solo puede contener caracteres alfanuméricos, guiones y guiones bajos',
- }),
- })
- }
-
- const gatewayConfig = PAYMENT_GATEWAYS[gateway]
- return z.object({
- publicKey: z
- .string()
- .min(10, { message: 'La clave pública debe tener al menos 10 caracteres' })
- .regex(gatewayConfig.publicKeyPattern, {
- message: `La clave pública debe comenzar con '${gatewayConfig.publicKeyPrefix}' seguido del formato correcto`,
- }),
- privateKey: z
- .string()
- .min(10, { message: 'La clave privada debe tener al menos 10 caracteres' })
- .regex(gatewayConfig.privateKeyPattern, {
- message: `La clave privada debe comenzar con '${gatewayConfig.privateKeyPrefix}' seguido del formato correcto`,
- }),
- })
-}
-
type ApiKeyFormValues = {
publicKey: string
privateKey: string
diff --git a/app/store/components/payments/PaymentSettings.tsx b/app/store/components/payments/PaymentSettings.tsx
index 31436afc..6ab77a3b 100644
--- a/app/store/components/payments/PaymentSettings.tsx
+++ b/app/store/components/payments/PaymentSettings.tsx
@@ -49,11 +49,11 @@ export function PaymentSettings() {
queryKey: ['storePaymentInfo', storeId],
queryFn: () => getStorePaymentInfo(storeId),
enabled: !!storeId,
- staleTime: Infinity, // Los datos nunca se consideran obsoletos
- gcTime: Infinity, // Los datos permanecen en caché indefinidamente
- refetchOnWindowFocus: false, // No refetch al enfocar la ventana
- refetchOnMount: false, // No refetch al montar el componente
- refetchOnReconnect: false, // No refetch al reconectar
+ staleTime: 5 * 60 * 1000, // Los datos se consideran frescos por 5 minutos
+ gcTime: 5 * 60 * 1000, // Los datos se eliminan de caché después de 5 minutos sin usarse
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
})
const storeRecordId = data?.id || null
@@ -94,7 +94,7 @@ export function PaymentSettings() {
toast.error('Error de configuración', {
description: 'Uyps! Hubo un error al configurar la pasarela de pago.',
})
- console.error('No se encontró el ID del registro de la tienda')
+ console.error('Store record ID not found')
return false
}
@@ -113,7 +113,7 @@ export function PaymentSettings() {
if (encryptedPublicKey) {
configData.publicKey = encryptedPublicKey
} else {
- console.error('Error al encriptar la clave pública de Wompi')
+ console.error('Error encrypting Wompi public key')
toast.error('Error de configuración', {
description:
'No se pudo configurar la pasarela de pago. Por favor, intenta nuevamente.',
@@ -133,7 +133,7 @@ export function PaymentSettings() {
if (encryptedSignature) {
configData.signature = encryptedSignature
} else {
- console.error('Error al encriptar la firma de Wompi')
+ console.error('Error encrypting Wompi signature')
toast.error('Error de configuración', {
description:
'No se pudo configurar la pasarela de pago. Por favor, intenta nuevamente.',
@@ -153,7 +153,7 @@ export function PaymentSettings() {
if (encryptedPublicKey) {
configData.publicKey = encryptedPublicKey
} else {
- console.error('Error al encriptar la clave pública de Mercado Pago')
+ console.error('Error encrypting the Mercado Pago public key')
toast.error('Error de configuración', {
description:
'No se pudo configurar la pasarela de pago. Por favor, intenta nuevamente.',
@@ -173,7 +173,7 @@ export function PaymentSettings() {
if (encryptedPrivateKey) {
configData.privateKey = encryptedPrivateKey
} else {
- console.error('Error al encriptar la clave privada de Mercado Pago')
+ console.error('Error encrypting the Mercado Pago private key')
toast.error('Error de configuración', {
description:
'No se pudo configurar la pasarela de pago. Por favor, intenta nuevamente.',
@@ -184,7 +184,6 @@ export function PaymentSettings() {
}
if (isEncrypting) {
- // Si todavía está encriptando, mostrar un mensaje genérico
toast.loading('Configurando pasarela de pago...')
return false
}
@@ -197,7 +196,7 @@ export function PaymentSettings() {
return success
} catch (err) {
- console.error('Error al configurar la pasarela de pago:', err)
+ console.error('Error configuring the payment gateway:', err)
toast.error('Error de configuración', {
description: 'No se pudo configurar la pasarela de pago. Por favor, intenta nuevamente.',
})
diff --git a/app/store/components/product-management/ProductForm.tsx b/app/store/components/product-management/ProductForm.tsx
index 3285a872..da004fdd 100644
--- a/app/store/components/product-management/ProductForm.tsx
+++ b/app/store/components/product-management/ProductForm.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
-import { Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { Button } from '@/components/ui/button'
import { Form } from '@/components/ui/form'
import { Card, CardContent } from '@/components/ui/card'
@@ -309,7 +309,7 @@ export function ProductForm({ storeId, productId }: ProductFormProps) {
>
{isSubmitting ? (
<>
-
+
Actualizando...
>
) : (
@@ -324,7 +324,7 @@ export function ProductForm({ storeId, productId }: ProductFormProps) {
>
{isSubmitting ? (
<>
-
+
Creando...
>
) : (
diff --git a/app/store/components/product-management/ProductList.tsx b/app/store/components/product-management/ProductList.tsx
index 9e20d38d..cd5359c9 100644
--- a/app/store/components/product-management/ProductList.tsx
+++ b/app/store/components/product-management/ProductList.tsx
@@ -1,7 +1,7 @@
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
import { routes } from '@/utils/routes'
-import { Loader2 } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
// Hooks
import { useProductFilters } from '@/app/store/components/product-management/hooks/useProductFilters'
@@ -125,7 +125,7 @@ export function ProductList({
{loading && (
-
+
Cargando productos...
)}
diff --git a/app/store/components/product-management/collection-form/form-page.tsx b/app/store/components/product-management/collection-form/form-page.tsx
index a4894b91..c0c707b6 100644
--- a/app/store/components/product-management/collection-form/form-page.tsx
+++ b/app/store/components/product-management/collection-form/form-page.tsx
@@ -1,5 +1,5 @@
-import { useState, useEffect } from 'react'
-import { ArrowLeft, Edit, Loader2 } from 'lucide-react'
+import { ArrowLeft, Edit } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
@@ -209,7 +209,7 @@ export function FormPage() {
>
{isSubmitting ? (
<>
-
+
Guardando...
>
) : (
diff --git a/app/store/components/product-management/collection-form/product-section.tsx b/app/store/components/product-management/collection-form/product-section.tsx
index e9c3a4a9..fbd0c4e6 100644
--- a/app/store/components/product-management/collection-form/product-section.tsx
+++ b/app/store/components/product-management/collection-form/product-section.tsx
@@ -36,7 +36,6 @@ export function ProductSection({
const [sortOption, setSortOption] = useState('mas-recientes')
const { storeId } = useStoreDataStore()
- // Usar el hook useProducts para cargar los productos
const { products, loading } = useProducts(storeId ?? undefined, {
limit: 100,
sortDirection: 'DESC',
@@ -51,7 +50,7 @@ export function ProductSection({
if (isDialogOpen) {
setDialogSelectedProducts(selectedProducts.map(p => p.id))
}
- }, [isDialogOpen]) // Solo se ejecuta cuando el diálogo se abre, no cuando selectedProducts cambia
+ }, [isDialogOpen])
// Filtrar productos basados en el término de búsqueda
const filteredProducts = products.filter(
diff --git a/app/store/components/product-management/hooks/usePriceSuggestion.ts b/app/store/components/product-management/hooks/usePriceSuggestion.ts
index 4fd2f7d7..5640d960 100644
--- a/app/store/components/product-management/hooks/usePriceSuggestion.ts
+++ b/app/store/components/product-management/hooks/usePriceSuggestion.ts
@@ -69,16 +69,16 @@ export function usePriceSuggestion() {
})
if (errors) {
- throw new Error(errors[0]?.message || 'Error en la generación de sugerencia de precio')
+ throw new Error(errors[0]?.message || 'Error generating price suggestion')
} else if (data) {
setResult(data as PriceSuggestionResult)
return data as PriceSuggestionResult
} else {
- throw new Error('No se recibió respuesta del servicio')
+ throw new Error('No response was received from the service')
}
} catch (err: any) {
- console.error('Error al generar sugerencia de precio:', err)
- const errorMessage = err.message || 'Error desconocido'
+ console.error('Error generating price suggestion:', err)
+ const errorMessage = err.message || 'Unknown error'
setError(new Error(errorMessage))
throw new Error(errorMessage)
} finally {
diff --git a/app/store/components/product-management/hooks/useProductDescription.ts b/app/store/components/product-management/hooks/useProductDescription.ts
index 6e4c5262..dc2a6801 100644
--- a/app/store/components/product-management/hooks/useProductDescription.ts
+++ b/app/store/components/product-management/hooks/useProductDescription.ts
@@ -58,16 +58,16 @@ export function useProductDescription() {
})
if (errors) {
- throw new Error(errors[0]?.message || 'Error en la generación de descripción')
+ throw new Error(errors[0]?.message || 'Error in generating description')
} else if (data) {
setDescription(data)
return data
} else {
- throw new Error('No se recibió respuesta del servicio')
+ throw new Error('No response was received from the service')
}
} catch (err: any) {
- console.error('Error al generar descripción:', err)
- const errorMessage = err.message || 'Error desconocido'
+ console.error('Error generating description:', err)
+ const errorMessage = err.message || 'Unknown error'
setError(new Error(errorMessage))
throw new Error(errorMessage)
} finally {
diff --git a/app/store/components/store-config/LogoUploader.tsx b/app/store/components/store-config/LogoUploader.tsx
index 2b526e78..97aa0698 100644
--- a/app/store/components/store-config/LogoUploader.tsx
+++ b/app/store/components/store-config/LogoUploader.tsx
@@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { toast } from 'sonner'
import Image from 'next/image'
-import { Upload, ImageIcon, Loader2 } from 'lucide-react'
+import { Upload, ImageIcon } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { useLogoUpload } from '@/app/store/hooks/useLogoUpload'
import { useUserStoreData } from '@/app/(without-navbar)/first-steps/hooks/useUserStoreData'
import useStoreDataStore from '@/zustand-states/storeDataStore'
@@ -26,7 +27,6 @@ export function LogoUploader() {
const { uploadLogo, status, error, reset } = useLogoUpload()
const { updateUserStore } = useUserStoreData()
-
const { currentStore, isLoading } = useStoreDataStore()
useEffect(() => {
@@ -162,7 +162,7 @@ export function LogoUploader() {
if (isLoading) {
return (
-
+
Cargando...
)
@@ -244,7 +244,7 @@ export function LogoUploader() {
>
{status === 'uploading' ? (
<>
-
+
Subiendo...
>
) : (
@@ -314,7 +314,7 @@ export function LogoUploader() {
>
{status === 'uploading' ? (
<>
-
+
Subiendo...
>
) : (
diff --git a/app/store/hooks/useApiKeyDecryption.ts b/app/store/hooks/useApiKeyDecryption.ts
index afeb4af5..22fd2fbb 100644
--- a/app/store/hooks/useApiKeyDecryption.ts
+++ b/app/store/hooks/useApiKeyDecryption.ts
@@ -49,7 +49,7 @@ export function useApiKeyDecryption() {
return null
} catch (error) {
- console.error('Error descifrando clave API:', error)
+ console.error('Error decrypting API key:', error)
return null
} finally {
setIsDecrypting(false)
diff --git a/app/store/hooks/useCollections.ts b/app/store/hooks/useCollections.ts
index e3e5779a..09534269 100644
--- a/app/store/hooks/useCollections.ts
+++ b/app/store/hooks/useCollections.ts
@@ -7,7 +7,6 @@ const client = generateClient
()
// Clave base para las consultas de colecciones
const COLLECTIONS_KEY = 'collections'
-const PRODUCTS_KEY = 'products'
/**
* Interfaz para los datos de entrada de una colección
@@ -47,7 +46,7 @@ export const useCollections = () => {
const result = await operation()
if (result.errors && result.errors.length > 0) {
setError(result.errors)
- throw new Error(result.errors[0].message || 'Error en la operación')
+ throw new Error(result.errors[0].message || 'Operation error')
}
return result.data
} catch (err) {
diff --git a/app/store/hooks/useLogoUpload.ts b/app/store/hooks/useLogoUpload.ts
index 08ae6ba1..8c9b3e55 100644
--- a/app/store/hooks/useLogoUpload.ts
+++ b/app/store/hooks/useLogoUpload.ts
@@ -73,7 +73,7 @@ export function useLogoUpload(): UseLogoUploadReturn {
}
} catch (err) {
setStatus('error')
- setError(err instanceof Error ? err.message : 'Ha ocurrido un error desconocido')
+ setError(err instanceof Error ? err.message : 'An unknown error has occurred')
console.error('Error uploading logo:', err)
return null
}
diff --git a/app/store/hooks/useProducts.ts b/app/store/hooks/useProducts.ts
index c8c92abe..f4d992d6 100644
--- a/app/store/hooks/useProducts.ts
+++ b/app/store/hooks/useProducts.ts
@@ -317,7 +317,7 @@ export function useProducts(
// Consulta para obtener un producto específico
const fetchProductById = async (id: string): Promise => {
if (!storeId) {
- console.error('No se puede obtener el producto: storeId no definido')
+ console.error('Cannot get product: storeId not defined')
return null
}
@@ -339,7 +339,7 @@ export function useProducts(
return existingProduct
} else {
console.error(
- `Acceso denegado: El producto ${id} no pertenece a la tienda actual ${storeId}`
+ `Access denied: Product ${id} does not belong to the current store ${storeId}`
)
return null
}
@@ -365,12 +365,12 @@ export function useProducts(
} else {
// El producto no pertenece a la tienda actual o no existe
console.error(
- `Acceso denegado: El producto ${id} no pertenece a la tienda actual ${storeId}`
+ `Access denied: Product ${id} does not belong to the current store ${storeId}`
)
return null
}
} catch (error) {
- console.error(`Error al verificar el producto ${id}:`, error)
+ console.error(`Error verifying product ${id}:`, error)
return null
}
}
@@ -402,7 +402,7 @@ export function useProducts(
try {
return await createProductMutation.mutateAsync(productData)
} catch (err) {
- console.error('Error al crear producto:', err)
+ console.error('Error creating product:', err)
return null
}
},
@@ -410,7 +410,7 @@ export function useProducts(
try {
return await updateProductMutation.mutateAsync(productData)
} catch (err) {
- console.error('Error al actualizar producto:', err)
+ console.error('Error updating product:', err)
return null
}
},
@@ -419,7 +419,7 @@ export function useProducts(
await deleteProductMutation.mutateAsync(id)
return true
} catch (err) {
- console.error('Error al eliminar producto:', err)
+ console.error('Error deleting product:', err)
return false
}
},
@@ -428,13 +428,11 @@ export function useProducts(
await deleteMultipleProductsMutation.mutateAsync(ids)
return true
} catch (err) {
- console.error('Error al eliminar múltiples productos:', err)
+ console.error('Error deleting multiple products:', err)
return false
}
},
fetchProduct: fetchProductById,
-
- // Otras funciones útiles
refreshProducts: () => refetch(),
}
}
diff --git a/components/ui/unsaved-changes-alert.tsx b/components/ui/unsaved-changes-alert.tsx
index 4b2658a9..79bd4ba9 100644
--- a/components/ui/unsaved-changes-alert.tsx
+++ b/components/ui/unsaved-changes-alert.tsx
@@ -2,7 +2,8 @@
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
-import { AlertCircle, Loader2 } from 'lucide-react'
+import { AlertCircle } from 'lucide-react'
+import { Loader } from '@/components/ui/loader'
import { motion, AnimatePresence } from 'framer-motion'
interface UnsavedChangesAlertProps {
@@ -80,7 +81,7 @@ export function UnsavedChangesAlert({
>
{isSaving ? (
<>
-
+
Guardando...
>
) : (
diff --git a/hooks/auth/useAuth.ts b/hooks/auth/useAuth.ts
index 368c025e..25f9012d 100644
--- a/hooks/auth/useAuth.ts
+++ b/hooks/auth/useAuth.ts
@@ -78,7 +78,7 @@ export const useAuth = () => {
clearUser()
}
} catch (error) {
- console.error('Error al obtener el usuario:', error)
+ console.error('Error getting user:', error)
clearUser()
} finally {
setLoading(false)
diff --git a/lib/schemas/api-keys.ts b/lib/schemas/api-keys.ts
new file mode 100644
index 00000000..9b297453
--- /dev/null
+++ b/lib/schemas/api-keys.ts
@@ -0,0 +1,72 @@
+// src/lib/validation/apiKeys.ts
+import { z } from 'zod'
+
+export const PAYMENT_GATEWAYS = {
+ mercadoPago: {
+ name: 'Mercado Pago',
+ transactionFee: 3.99,
+ publicKeyPrefix: 'APP_USR-',
+ privateKeyPrefix: 'TEST-',
+ publicKeyPattern: /^APP_USR-[a-zA-Z0-9-]+$/,
+ privateKeyPattern: /^TEST-[a-zA-Z0-9-]+$/,
+ publicKeyPlaceholder: 'APP_USR-1234567890123456',
+ privateKeyPlaceholder: 'Kw4aC0rZVgLZQn209NbEKPuXLzBD28Zx',
+ color: 'bg-blue-100 text-blue-800 border-blue-200',
+ description: 'Popular en América Latina con amplio soporte regional.',
+ publicKeyLabel: 'Access Token',
+ privateKeyLabel: 'Clave Secreta',
+ },
+ wompi: {
+ name: 'Wompi',
+ transactionFee: 2.9,
+ publicKeyPrefix: 'pub_',
+ privateKeyPrefix: 'prv_',
+ publicKeyPattern: /^(pub_test|pub_prod|prod)_[a-zA-Z0-9]{16,}$/,
+ privateKeyPattern: /^[a-zA-Z0-9_-]+$/,
+ publicKeyPlaceholder: 'pub_prod_1234567890abcdef',
+ privateKeyPlaceholder: 'prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqs',
+ color: 'bg-purple-100 text-purple-800 border-purple-200',
+ description: 'Tarifas más bajas con fuerte soporte en Colombia y otras regiones.',
+ publicKeyLabel: 'Llave Pública',
+ privateKeyLabel: 'Firma (Signature)',
+ },
+} as const
+
+export type PaymentGatewayType = keyof typeof PAYMENT_GATEWAYS
+
+export const createApiKeySchema = (gateway: PaymentGatewayType) => {
+ const config = PAYMENT_GATEWAYS[gateway]
+
+ if (gateway === 'wompi') {
+ return z.object({
+ publicKey: z
+ .string()
+ .min(16, { message: 'La llave pública debe tener al menos 16 caracteres' })
+ .regex(config.publicKeyPattern, {
+ message: 'Formato de llave pública inválido. Debe comenzar con pub_ o prod_',
+ }),
+ privateKey: z
+ .string()
+ .min(32, { message: 'La firma debe tener al menos 32 caracteres' })
+ .max(128, { message: 'La firma no puede exceder 128 caracteres' })
+ .regex(config.privateKeyPattern, {
+ message: 'La firma solo puede contener caracteres alfanuméricos, guiones y guiones bajos',
+ }),
+ })
+ }
+
+ return z.object({
+ publicKey: z
+ .string()
+ .min(10, { message: 'La clave pública debe tener al menos 10 caracteres' })
+ .regex(config.publicKeyPattern, {
+ message: `La clave pública debe comenzar con '${config.publicKeyPrefix}' seguido del formato correcto`,
+ }),
+ privateKey: z
+ .string()
+ .min(10, { message: 'La clave privada debe tener al menos 10 caracteres' })
+ .regex(config.privateKeyPattern, {
+ message: `La clave privada debe comenzar con '${config.privateKeyPrefix}' seguido del formato correcto`,
+ }),
+ })
+}
diff --git a/lib/schemas/email-change.ts b/lib/schemas/email-change.ts
index 8de6e469..4655d8bd 100644
--- a/lib/schemas/email-change.ts
+++ b/lib/schemas/email-change.ts
@@ -7,3 +7,10 @@ export const emailSchema = z.object({
export const verificationCodeSchema = z.object({
verificationCode: z.string().min(6, 'El código debe tener al menos 6 caracteres'),
})
+
+export const formSchema = z.object({
+ firstName: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'),
+ lastName: z.string().min(2, 'El apellido debe tener al menos 2 caracteres'),
+ phone: z.string().min(10, 'El teléfono debe tener al menos 10 caracteres'),
+ bio: z.string(),
+})
diff --git a/lib/schemas/password-change.ts b/lib/schemas/password-change.ts
new file mode 100644
index 00000000..389c7f53
--- /dev/null
+++ b/lib/schemas/password-change.ts
@@ -0,0 +1,20 @@
+import * as z from 'zod'
+
+export const passwordSchema = z
+ .object({
+ oldPassword: z.string().min(1, 'La contraseña actual es requerida'),
+ newPassword: z
+ .string()
+ .min(8, 'La nueva contraseña debe tener al menos 8 caracteres')
+ .regex(
+ /[!@#$%^&*()\-_=+{};:,<.>]/,
+ 'La contraseña debe contener al menos una letra mayúscula, una minúscula, un número y un carácter especial'
+ ),
+ confirmPassword: z.string().min(1, 'Confirma tu nueva contraseña'),
+ })
+ .refine(data => data.newPassword === data.confirmPassword, {
+ message: 'Las contraseñas no coinciden',
+ path: ['confirmPassword'],
+ })
+
+export type PasswordFormValues = z.infer
diff --git a/zustand-states/useSubscriptionStore.ts b/zustand-states/useSubscriptionStore.ts
index 649b7f14..87d6d715 100644
--- a/zustand-states/useSubscriptionStore.ts
+++ b/zustand-states/useSubscriptionStore.ts
@@ -4,7 +4,7 @@ import { type Schema } from '@/amplify/data/resource'
const client = generateClient()
-// Definimos un tipo con solo los campos necesarios
+// tipo con solo los campos necesarios
interface MinimalSubscription {
subscriptionId: Schema['UserSubscription']['type']['subscriptionId']
planName: Schema['UserSubscription']['type']['planName']
@@ -20,66 +20,124 @@ interface SubscriptionState {
loading: boolean
error: string | null
setCognitoUsername: (username: string | null) => void
- fetchSubscription: () => Promise
+ fetchSubscription: () => Promise
+ subscriptionResource: {
+ read: () => MinimalSubscription | null
+ preload: (username: string) => void
+ }
}
-export const useSubscriptionStore = create((set, get) => ({
- cognitoUsername: null,
- subscription: null,
- loading: false,
- error: null,
- setCognitoUsername: username => set({ cognitoUsername: username }),
- fetchSubscription: async () => {
- const { cognitoUsername } = get()
-
- if (!cognitoUsername) {
- set({ subscription: null, loading: false, error: null })
- return
- }
+function createResource() {
+ let status = 'pending'
+ let result: MinimalSubscription | null = null
+ let error: Error | null = null
+ let suspender: Promise | null = null
- set({ loading: true, error: null })
-
- try {
- const { data, errors } = await client.models.UserSubscription.list({
- filter: { userId: { eq: cognitoUsername } },
- selectionSet: [
- 'subscriptionId',
- 'planName',
- 'pendingPlan',
- 'nextPaymentDate',
- 'lastFourDigits',
- 'createdAt',
- ],
-
- authMode: 'userPool',
- })
-
- if (errors && errors.length > 0) {
- throw new Error('Error al obtener la suscripción')
+ return {
+ read() {
+ if (status === 'pending' && suspender) {
+ throw suspender
+ } else if (status === 'error') {
+ throw error
+ } else {
+ return result
}
+ },
+ preload(username: string) {
+ if (!username) return
+
+ status = 'pending'
+ suspender = fetchSubscriptionData(username)
+ .then(data => {
+ status = 'success'
+ result = data
+ })
+ .catch(e => {
+ status = 'error'
+ error = e
+ })
+ },
+ }
+}
+
+// Función auxiliar para obtener los datos de suscripción
+async function fetchSubscriptionData(username: string): Promise {
+ try {
+ const { data, errors } = await client.models.UserSubscription.list({
+ filter: { userId: { eq: username } },
+ selectionSet: [
+ 'subscriptionId',
+ 'planName',
+ 'pendingPlan',
+ 'nextPaymentDate',
+ 'lastFourDigits',
+ 'createdAt',
+ ],
+ authMode: 'userPool',
+ })
+
+ if (errors && errors.length > 0) {
+ throw new Error('Error getting subscription')
+ }
+
+ const sortedData = data.sort(
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ )
+
+ if (sortedData.length === 0) {
+ return null
+ }
- const sortedData = data.sort(
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
- )
-
- const minimalSubscription: MinimalSubscription | null =
- sortedData.length > 0
- ? {
- subscriptionId: sortedData[0].subscriptionId,
- planName: sortedData[0].planName,
- pendingPlan: sortedData[0].pendingPlan,
- nextPaymentDate: sortedData[0].nextPaymentDate,
- lastFourDigits: sortedData[0].lastFourDigits,
- createdAt: sortedData[0].createdAt,
- }
- : null
-
- set({
- subscription: minimalSubscription,
- loading: false,
- })
- } catch (error) {
- set({ error: 'Error al cargar la suscripción', loading: false })
+ return {
+ subscriptionId: sortedData[0].subscriptionId,
+ planName: sortedData[0].planName,
+ pendingPlan: sortedData[0].pendingPlan,
+ nextPaymentDate: sortedData[0].nextPaymentDate,
+ lastFourDigits: sortedData[0].lastFourDigits,
+ createdAt: sortedData[0].createdAt,
}
- },
-}))
+ } catch (error) {
+ console.error('Error fetching subscription:', error)
+ throw error
+ }
+}
+
+export const useSubscriptionStore = create((set, get) => {
+ const subscriptionResource = createResource()
+
+ return {
+ cognitoUsername: null,
+ subscription: null,
+ loading: false,
+ error: null,
+ subscriptionResource,
+ setCognitoUsername: username => {
+ set({ cognitoUsername: username })
+ if (username) {
+ subscriptionResource.preload(username)
+ }
+ },
+ fetchSubscription: async () => {
+ const { cognitoUsername } = get()
+
+ if (!cognitoUsername) {
+ set({ subscription: null, loading: false, error: null })
+ return null
+ }
+
+ set({ loading: true, error: null })
+
+ try {
+ const subscription = await fetchSubscriptionData(cognitoUsername)
+ set({
+ subscription,
+ loading: false,
+ })
+ return subscription
+ } catch (error) {
+ set({ error: 'Error loading subscription', loading: false })
+ return null
+ }
+ },
+ }
+})