diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..b400dbe4 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,93 @@ +# Configuración de labels automáticos para el proyecto Fasttify +# Basado en los archivos modificados en pull requests + +# Frontend y componentes UI +frontend: + - app/**/*.tsx + - app/**/*.ts + - components/**/*.tsx + - components/**/*.ts + - packages/theme-editor/**/* + - packages/orders-app/**/* + +# Amplify y backend +backend: + - amplify/**/* + - app/api/**/* + - lib/**/*.ts + - utils/**/*.ts + - middlewares/**/*.ts + +# Funciones Lambda +lambda: + - amplify/functions/**/* + - amplify/data/functions/**/* + +# Configuración y DevOps +config: + - '**/*.json' + - '**/*.yml' + - '**/*.yaml' + - '**/*.config.*' + - next.config.* + - tailwind.config.* + - tsconfig*.json + - package*.json + - pnpm-*.yaml + +# Temas y plantillas +themes: + - packages/example-themes/**/* + - packages/liquid-forge/**/* + - app/themes/**/* + +# Documentación +documentation: + - docs/**/* + - '**/*.md' + - README.md + - CONTRIBUTING.md + - SECURITY.md + - CODE_OF_CONDUCT.md + +# Tests +tests: + - test/**/* + - '**/*.test.*' + - '**/*.spec.*' + - jest.* + - __tests__/**/* + +# Scripts y utilidades +scripts: + - scripts/**/* + +# Email templates +emails: + - packages/emails/**/* + +# Estilos y CSS +styling: + - app/global.css + - '**/*.css' + - '**/*.scss' + - '**/*.sass' + +# Imágenes y assets +assets: + - packages/image-optimization/**/* + - '**/*.png' + - '**/*.jpg' + - '**/*.jpeg' + - '**/*.gif' + - '**/*.svg' + - '**/*.webp' + +# Dominios y infraestructura +infrastructure: + - packages/tenant-domains/**/* + - packages/lambda-edge-host-rewriter/**/* + +# Workflows de GitHub Actions +github-actions: + - .github/**/* diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..96668e50 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,23 @@ +name: Labeler + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Label PR + uses: actions/labeler@v4 + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/amplify/auth/resource.ts b/amplify/auth/resource.ts index df0415cf..83db4ccd 100644 --- a/amplify/auth/resource.ts +++ b/amplify/auth/resource.ts @@ -29,6 +29,7 @@ export const auth = defineAuth({ email: 'email', nickname: 'name', profilePicture: 'picture', + emailVerified: 'email_verified', }, }, diff --git a/app/store/components/profile/components/SubscriptionSection.tsx b/app/store/components/profile/components/SubscriptionSection.tsx index c8d7cfbc..01723bf4 100644 --- a/app/store/components/profile/components/SubscriptionSection.tsx +++ b/app/store/components/profile/components/SubscriptionSection.tsx @@ -1,8 +1,9 @@ import { Card, Text, Button, Banner, SkeletonBodyText, Icon } from '@shopify/polaris'; import { ExternalIcon, CheckCircleIcon } from '@shopify/polaris-icons'; -import { useState } from 'react'; +import { useState, memo } from 'react'; import { plans } from '@/app/(www)/pricing/components/plans'; import type { UserProps } from '@/app/store/components/profile/types'; +import { useSubscriptionLogic } from '@/app/store/hooks/useSubscriptionLogic'; interface SubscriptionSectionProps extends UserProps { storeId: string; @@ -15,8 +16,9 @@ interface SubscriptionSectionProps extends UserProps { * @param {SubscriptionSectionProps} props - Propiedades del componente * @returns {JSX.Element} Sección de suscripción con redirección a Polarsh */ -export function SubscriptionSection({ user, loading }: SubscriptionSectionProps) { +const SubscriptionSectionComponent = ({ user, loading }: SubscriptionSectionProps) => { const [isSubmitting, setIsSubmitting] = useState(false); + const { subscriptionLoading, hasRealSubscription, isPaidPlan, currentPlan } = useSubscriptionLogic(user?.userId); /** * Maneja la redirección a Polarsh para gestionar la suscripción @@ -30,11 +32,8 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps) setIsSubmitting(true); try { - // Si el usuario tiene un plan pagado, ir al portal de gestión - // Si no, ir al checkout para suscribirse - const isPaidPlan = user.plan && user.plan !== 'Gratuito'; - - if (isPaidPlan) { + // Usar hasRealSubscription del hook para determinar la acción + if (hasRealSubscription) { // Redirigir al customer portal const portalUrl = new URL('/api/portal', window.location.origin); portalUrl.searchParams.set('customerExternalId', user.userId); @@ -64,7 +63,7 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps) } }; - if (loading || !user) { + if (loading || !user || subscriptionLoading) { return (
@@ -74,8 +73,7 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps) ); } - const currentPlan = user.plan || 'Gratuito'; - const isPaidPlan = currentPlan !== 'Gratuito'; + // Los valores ya vienen del hook return ( @@ -124,14 +122,22 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps)
)} + {isPaidPlan && !hasRealSubscription && ( +
+ +

Tienes un plan de prueba. Para gestionar tu suscripción, primero actualiza a un plan premium.

+
+
+ )} +
@@ -142,4 +148,6 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps)
); -} +}; + +export const SubscriptionSection = memo(SubscriptionSectionComponent); diff --git a/app/store/hooks/useSubscriptionLogic.ts b/app/store/hooks/useSubscriptionLogic.ts new file mode 100644 index 00000000..83371858 --- /dev/null +++ b/app/store/hooks/useSubscriptionLogic.ts @@ -0,0 +1,85 @@ +import { useEffect, useMemo, useRef, useCallback } from 'react'; +import { useSubscriptionStore } from '@/context/core/useSubscriptionStore'; + +interface UseSubscriptionLogicResult { + subscription: any; + subscriptionLoading: boolean; + hasRealSubscription: boolean; + isPaidPlan: boolean; + currentPlan: string; +} + +/** + * Hook personalizado para manejar la lógica de suscripciones + * Evita múltiples peticiones y proporciona datos limpios + */ +export function useSubscriptionLogic(userId?: string): UseSubscriptionLogicResult { + const { subscription, loading: subscriptionLoading, setCognitoUsername, fetchSubscription } = useSubscriptionStore(); + const initializedUsers = useRef(new Set()); + const loadingUsers = useRef(new Set()); + + // Función memoizada para cargar la suscripción + const loadSubscription = useCallback( + async (id: string) => { + if (loadingUsers.current.has(id)) { + return; // Ya se está cargando + } + + loadingUsers.current.add(id); + try { + setCognitoUsername(id); + await fetchSubscription(); + initializedUsers.current.add(id); + } finally { + loadingUsers.current.delete(id); + } + }, + [setCognitoUsername, fetchSubscription] + ); + + // Cargar datos de suscripción solo una vez por usuario + useEffect(() => { + // Solo ejecutar si userId es válido y no es undefined + if (!userId) { + return; + } + + if (!initializedUsers.current.has(userId) && !loadingUsers.current.has(userId)) { + loadSubscription(userId); + } + }, [userId, loadSubscription]); + + // Detectar si es suscripción real de Polar (memoizada) + const hasRealSubscription = useMemo(() => { + if (!subscription) return false; + + // Verificar que tiene subscriptionId y no es trial + const hasValidSubscriptionId = Boolean( + subscription.subscriptionId && !subscription.subscriptionId.startsWith('trial-') + ); + + // Verificar que tiene planPrice mayor a 0 + const hasValidPrice = (subscription.planPrice ?? 0) > 0; + + return hasValidSubscriptionId && hasValidPrice; + }, [subscription]); + + // Determinar si es plan pagado basado en el precio del plan + const isPaidPlan = useMemo(() => { + if (!subscription) return false; + return (subscription.planPrice ?? 0) > 0; + }, [subscription]); + + // Obtener el plan actual + const currentPlan = useMemo(() => { + return subscription?.planName || 'Gratuito'; + }, [subscription]); + + return { + subscription, + subscriptionLoading, + hasRealSubscription, + isPaidPlan, + currentPlan, + }; +} diff --git a/context/core/useSubscriptionStore.ts b/context/core/useSubscriptionStore.ts index d7cfe534..ddc8ebe6 100644 --- a/context/core/useSubscriptionStore.ts +++ b/context/core/useSubscriptionStore.ts @@ -92,22 +92,24 @@ export const useSubscriptionStore = create((set, get) => { subscriptionResource, setCognitoUsername: (username) => { set({ cognitoUsername: username }); - if (username) { - subscriptionResource.preload(username); - } }, fetchSubscription: async () => { - const { cognitoUsername } = get(); + const state = get(); - if (!cognitoUsername) { + if (!state.cognitoUsername) { set({ subscription: null, loading: false, error: null }); return null; } + // Evitar peticiones duplicadas si ya está cargando + if (state.loading) { + return state.subscription; + } + set({ loading: true, error: null }); try { - const subscription = await fetchSubscriptionData(cognitoUsername); + const subscription = await fetchSubscriptionData(state.cognitoUsername); set({ subscription, loading: false,