From 0f651d348c47ef4d9b1b226752bcf3ed090034f3 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 00:21:56 -0500
Subject: [PATCH 1/9] chore: update project name and migrate to Amplify Gen2
Update the project name in the Amplify configuration and migrate the README to reflect the use of AWS Amplify Gen2. This includes updating instructions, features, and benefits to align with Gen2's TypeScript-first approach and improved developer experience.
---
README.md | 72 +++++++++++++++++++++++------
amplify/.config/project-config.json | 2 +-
2 files changed, 60 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 670ae490..dceffdf6 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
@@ -84,3 +128,5 @@ This project is licensed under the MIT-0 License. See the [LICENSE](LICENSE) fil
---
### Build your dream dropshipping business with Fasttify today! 🚀✨
+
+ Too many current requests. Your queue position is 1. Please wait for a while or switch to other models for a smoother experience.
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": {
From b4990767789fa8bdc7463dfe776fc23d13be0c67 Mon Sep 17 00:00:00 2001
From: Steven Jaime <143671152+Stivenjs@users.noreply.github.com>
Date: Wed, 7 May 2025 00:24:47 -0500
Subject: [PATCH 2/9] Update README.md
---
README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/README.md b/README.md
index dceffdf6..a1026f7a 100644
--- a/README.md
+++ b/README.md
@@ -128,5 +128,3 @@ This project is licensed under the MIT-0 License. See the [LICENSE](LICENSE) fil
---
### Build your dream dropshipping business with Fasttify today! 🚀✨
-
- Too many current requests. Your queue position is 1. Please wait for a while or switch to other models for a smoother experience.
From 3e9ff7fe975d631529aec74caa994487bcd3c172 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 11:16:01 -0500
Subject: [PATCH 3/9] ci: add SonarQube configuration and GitHub Actions
workflow
This commit introduces a SonarQube configuration file (`sonar-project.properties`) and a GitHub Actions workflow (`build.yml`) to enable code quality analysis. The workflow triggers on push to `main` and `dev` branches, as well as on pull request events, ensuring continuous integration and code quality checks.
---
.github/workflows/build.yml | 20 ++++++++++++++++++++
sonar-project.properties | 14 ++++++++++++++
2 files changed, 34 insertions(+)
create mode 100644 .github/workflows/build.yml
create mode 100644 sonar-project.properties
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..7f60354c
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,20 @@
+name: Build
+on:
+ push:
+ branches:
+ - main
+ - dev
+ pull_request:
+ types: [opened, synchronize, reopened]
+jobs:
+ sonarqube:
+ name: SonarQube
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v5
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 00000000..bf87f443
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,14 @@
+sonar.projectKey=Fasttify_fasttify
+sonar.organization=fasttify
+
+
+# This is the name and version displayed in the SonarCloud UI.
+#sonar.projectName=fasttify
+#sonar.projectVersion=1.0
+
+
+# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
+#sonar.sources=.
+
+# Encoding of the source code. Default is default system encoding
+#sonar.sourceEncoding=UTF-8
\ No newline at end of file
From 942b8ac1240c82dd830414383b2ff864ba5121e2 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 11:20:11 -0500
Subject: [PATCH 4/9] chore: remove commented-out configurations in
sonar-project.properties
Clean up the sonar-project.properties file by removing unused and commented-out configurations to improve readability and maintainability
---
sonar-project.properties | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/sonar-project.properties b/sonar-project.properties
index bf87f443..d557ef9a 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -2,13 +2,3 @@ sonar.projectKey=Fasttify_fasttify
sonar.organization=fasttify
-# This is the name and version displayed in the SonarCloud UI.
-#sonar.projectName=fasttify
-#sonar.projectVersion=1.0
-
-
-# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
-#sonar.sources=.
-
-# Encoding of the source code. Default is default system encoding
-#sonar.sourceEncoding=UTF-8
\ No newline at end of file
From b9757fd92d3dc494bd51f8c202f79ad475dc0657 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 13:14:46 -0500
Subject: [PATCH 5/9] refactor(account-settings): improve subscription handling
and UI loading states
Refactor subscription logic to use resource preloading and suspense for better performance. Simplify payment settings component by separating concerns into smaller components. Add loading states and error fallbacks for a smoother user experience. Remove unused disconnect button and update error messages to English.
---
.../components/AccountSettings.tsx | 3 -
.../components/PaymentSettings.tsx | 179 ++++++++++--------
.../account-settings/components/SideBar.tsx | 35 ++--
app/(with-navbar)/account-settings/page.tsx | 36 +++-
hooks/auth/useAuth.ts | 2 +-
zustand-states/useSubscriptionStore.ts | 174 +++++++++++------
6 files changed, 265 insertions(+), 164 deletions(-)
diff --git a/app/(with-navbar)/account-settings/components/AccountSettings.tsx b/app/(with-navbar)/account-settings/components/AccountSettings.tsx
index 69f8ee91..f615541d 100644
--- a/app/(with-navbar)/account-settings/components/AccountSettings.tsx
+++ b/app/(with-navbar)/account-settings/components/AccountSettings.tsx
@@ -158,9 +158,6 @@ export function AccountSettings() {
-
- Desconectar
-
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/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/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
+ }
+ },
+ }
+})
From 077b6469df4c6501bf67f7a295362999b55e0ae2 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 16:22:36 -0500
Subject: [PATCH 6/9] feat(auth): add id field to UserSubscription model and
handler
This commit introduces the `id` field to the `UserSubscription` model and updates the post-confirmation handler to include it. The changes ensure that each subscription has a unique identifier, improving data integrity and consistency. Additionally, the plan scheduler handler has been enhanced to process subscriptions more efficiently by checking expiration dates and pending plan changes.
---
amplify/auth/post-confirmation/handler.ts | 1 +
amplify/data/resource.ts | 2 ++
amplify/functions/planScheduler/handler.ts | 39 +++++++++++++++++-----
3 files changed, 33 insertions(+), 9 deletions(-)
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)
}
}
From 62a4e17c3072b1a3fe9abd3ca63231025aa6a69a Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 17:04:49 -0500
Subject: [PATCH 7/9] refactor: replace Lucide Loader2 with custom Loader
component
Replace all instances of `Loader2` from Lucide with a custom `Loader` component for consistency and maintainability. This change centralizes the loading spinner implementation and simplifies future updates.
---
.../components/AccountSettings.tsx | 2 +-
.../components/ChangeEmailDialog.tsx | 6 ++---
.../components/ChangePasswordDialog.tsx | 26 +++----------------
.../components/EditProfileDialog.tsx | 4 +--
.../forgot-password/ForgotPasswordForm.tsx | 10 ++++++-
.../login/components/sing-in/SignInForm.tsx | 5 ++--
.../login/components/sing-up/SignUpForm.tsx | 5 ++--
.../verification-form/VerificationForm.tsx | 4 +--
.../components/domains/ChangeDomainDialog.tsx | 7 ++---
.../domains/EditStoreProfileDialog.tsx | 10 ++++++-
.../images-selector/image-selector-modal.tsx | 7 ++---
.../product-management/ProductForm.tsx | 6 ++---
.../product-management/ProductList.tsx | 4 +--
.../collection-form/form-page.tsx | 6 ++---
.../components/store-config/LogoUploader.tsx | 10 +++----
components/ui/unsaved-changes-alert.tsx | 5 ++--
lib/schemas/password-change.ts | 20 ++++++++++++++
17 files changed, 80 insertions(+), 57 deletions(-)
create mode 100644 lib/schemas/password-change.ts
diff --git a/app/(with-navbar)/account-settings/components/AccountSettings.tsx b/app/(with-navbar)/account-settings/components/AccountSettings.tsx
index f615541d..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 {
diff --git a/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx b/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
index bdad3374..b4840b59 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,
@@ -149,7 +149,7 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
{loading ? (
-
+
Procesando...
) : (
@@ -185,7 +185,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..f0cbbac6 100644
--- a/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
@@ -15,7 +15,7 @@ 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 useAuthStore from '@/zustand-states/userStore'
@@ -155,7 +155,7 @@ export function EditProfileDialog({ open, onOpenChange }: EditProfileDialogProps
>
{loading ? (
-
+
Guardando
) : (
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/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/images-selector/image-selector-modal.tsx b/app/store/components/images-selector/image-selector-modal.tsx
index dba3cb64..41dce2d1 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'
@@ -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/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/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/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/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
From 581a15cbee42bb9da579e49a41d977b54e4de19f Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 17:19:14 -0500
Subject: [PATCH 8/9] refactor: clean up and optimize code across multiple
components
Remove unused imports, streamline form schema definitions, and improve error message consistency. Also, relocate the form schema to a shared file for better reusability and maintainability.
---
.../components/CancellationDialog.tsx | 2 +-
.../account-settings/components/ChangeEmailDialog.tsx | 1 -
.../account-settings/components/EditProfileDialog.tsx | 10 ++--------
app/(with-navbar)/landing/components/DocsLanding.tsx | 2 +-
app/(with-navbar)/landing/components/NavBar.tsx | 2 +-
app/(with-navbar)/page.tsx | 11 +++++------
lib/schemas/email-change.ts | 7 +++++++
7 files changed, 17 insertions(+), 18 deletions(-)
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 b4840b59..b9191c22 100644
--- a/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/ChangeEmailDialog.tsx
@@ -44,7 +44,6 @@ export function ChangeEmailDialog({ open, onOpenChange, currentEmail }: ChangeEm
})
const {
- register: registerVerification,
handleSubmit: handleSubmitVerification,
formState: { errors: verificationErrors },
reset: resetVerification,
diff --git a/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx b/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
index f0cbbac6..34fb2e45 100644
--- a/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
+++ b/app/(with-navbar)/account-settings/components/EditProfileDialog.tsx
@@ -18,15 +18,9 @@ import { useUserAttributes } from '@/app/(with-navbar)/account-settings/hooks/us
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(' ') : []
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/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(),
+})
From 5779727d436f1f5f7ba0acef6f3fd8a9e7a3a333 Mon Sep 17 00:00:00 2001
From: Stivenjs
Date: Wed, 7 May 2025 18:08:56 -0500
Subject: [PATCH 9/9] refactor(payments): move API key schema to shared lib and
update error messages
Extracted API key schema and related types to a shared library for reusability. Updated error messages to English for consistency across the codebase. Simplified cache configuration in PaymentSettings to use a 5-minute TTL.
---
app/store/components/ai-chat/hooks/useChat.ts | 8 +--
.../app-integration/ConnectModal.tsx | 13 +---
.../domains/utils/storeProfileUtils.ts | 7 +-
.../images-selector/image-selector-modal.tsx | 2 +-
app/store/components/payments/ApiKeyModal.tsx | 72 +------------------
.../components/payments/PaymentSettings.tsx | 23 +++---
.../collection-form/product-section.tsx | 3 +-
.../hooks/usePriceSuggestion.ts | 8 +--
.../hooks/useProductDescription.ts | 8 +--
app/store/hooks/useApiKeyDecryption.ts | 2 +-
app/store/hooks/useCollections.ts | 3 +-
app/store/hooks/useLogoUpload.ts | 2 +-
app/store/hooks/useProducts.ts | 18 +++--
lib/schemas/api-keys.ts | 72 +++++++++++++++++++
14 files changed, 115 insertions(+), 126 deletions(-)
create mode 100644 lib/schemas/api-keys.ts
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/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 41dce2d1..b40aa1d7 100644
--- a/app/store/components/images-selector/image-selector-modal.tsx
+++ b/app/store/components/images-selector/image-selector-modal.tsx
@@ -83,7 +83,7 @@ export default function ImageSelectorModal({
} finally {
setIsUploading(false)
setUploadPreview(null)
- // Limpiar el input de archivo
+
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
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/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/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/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`,
+ }),
+ })
+}