Skip to content
Merged

Cli #359

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -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/**/*
23 changes: 23 additions & 0 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions amplify/auth/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const auth = defineAuth({
email: 'email',
nickname: 'name',
profilePicture: 'picture',
emailVerified: 'email_verified',
},
},

Expand Down
34 changes: 21 additions & 13 deletions app/store/components/profile/components/SubscriptionSection.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -64,7 +63,7 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps)
}
};

if (loading || !user) {
if (loading || !user || subscriptionLoading) {
return (
<Card>
<div style={{ padding: '20px' }}>
Expand All @@ -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 (
<Card>
Expand Down Expand Up @@ -124,14 +122,22 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps)
</div>
)}

{isPaidPlan && !hasRealSubscription && (
<div style={{ marginBottom: '12px' }}>
<Banner tone="warning">
<p>Tienes un plan de prueba. Para gestionar tu suscripción, primero actualiza a un plan premium.</p>
</Banner>
</div>
)}

<Button
icon={ExternalIcon}
onClick={handleRedirectToPolarsh}
loading={isSubmitting}
variant={isPaidPlan ? undefined : 'primary'}
variant={hasRealSubscription ? undefined : 'primary'}
size="slim"
fullWidth>
{isSubmitting ? 'Redirigiendo...' : isPaidPlan ? 'Gestionar suscripción' : 'Actualizar plan'}
{isSubmitting ? 'Redirigiendo...' : hasRealSubscription ? 'Gestionar suscripción' : 'Actualizar plan'}
</Button>

<div style={{ marginTop: '12px' }}>
Expand All @@ -142,4 +148,6 @@ export function SubscriptionSection({ user, loading }: SubscriptionSectionProps)
</div>
</Card>
);
}
};

export const SubscriptionSection = memo(SubscriptionSectionComponent);
85 changes: 85 additions & 0 deletions app/store/hooks/useSubscriptionLogic.ts
Original file line number Diff line number Diff line change
@@ -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<string>());
const loadingUsers = useRef(new Set<string>());

// 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,
};
}
14 changes: 8 additions & 6 deletions context/core/useSubscriptionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,24 @@ export const useSubscriptionStore = create<SubscriptionState>((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,
Expand Down