Skip to content
Merged

Cli #355

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
2 changes: 2 additions & 0 deletions app/(setup)/login/components/sing-in/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function SignInForm({ onForgotPassword, onVerificationNeeded, redirectPat
<FormLabel>Correo electrónico</FormLabel>
<FormControl>
<Input
autoComplete="email"
placeholder="correo@ejemplo.com"
{...field}
onChange={(e) => {
Expand Down Expand Up @@ -92,6 +93,7 @@ export function SignInForm({ onForgotPassword, onVerificationNeeded, redirectPat
<FormControl>
<div className="relative">
<Input
autoComplete="current-password"
type={showPassword ? 'text' : 'password'}
placeholder="Ingresa tu contraseña"
{...field}
Expand Down
19 changes: 12 additions & 7 deletions app/(setup)/my-store/components/AnimatedBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ interface AnimatedBackgroundProps {
backgroundColor?: string;
shapeColor1?: string;
shapeColor2?: string;
containerClassName?: string;
isModal?: boolean;
}

export function AnimatedBackground({
minWidth = '1024px',
backgroundColor = 'rgba(20, 20, 20, 1)',
shapeColor1 = 'rgba(255, 123, 142, 1)',
shapeColor2 = 'rgba(123, 255, 142, 1)',
shapeColor1 = 'rgba(142, 123, 255, 1)',
shapeColor2 = 'rgba(68, 242, 235, 1)',
containerClassName = '',
isModal = false,
}: AnimatedBackgroundProps) {
const [shouldRender, setShouldRender] = useState(false);

Expand Down Expand Up @@ -62,7 +66,7 @@ export function AnimatedBackground({
75% {
transform: translate(-30%, 40%) skew(15deg, 15deg) rotate(240deg);
}
100% {
to {
transform: translate(-30%, 40%) rotate(-20deg);
}
}
Expand All @@ -83,20 +87,21 @@ export function AnimatedBackground({
80% {
transform: translate(10%, -30%) rotate(180deg);
}
100% {
to {
transform: translate(20%, -40%) rotate(340deg);
}
}

.animated-wrapper {
position: fixed;
position: ${isModal ? 'absolute' : 'fixed'};
top: 0;
left: 0;
overflow: hidden;
width: 100%;
height: 100%;
background: var(--background-color);
z-index: -1;
z-index: ${isModal ? '1' : '-1'};
border-radius: ${isModal ? '1rem' : '0'};
}

.shape-container {
Expand Down Expand Up @@ -182,7 +187,7 @@ export function AnimatedBackground({
}
`}</style>

<div className="animated-wrapper">
<div className={`animated-wrapper ${containerClassName}`}>
<div className="shape-container">
<div className="shape"></div>
<div className="shape"></div>
Expand Down
175 changes: 38 additions & 137 deletions app/(www)/pricing/components/PricingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
import outputs from '@/amplify_outputs.json';
import { Button } from '@/components/ui/button';
import { LoadingIndicator } from '@/components/ui/loading-indicator';
import { Toast } from '@/components/ui/toasts';
import { useAuth } from '@/context/hooks/useAuth';
import { useIsClient } from '@/hooks/ui/useIsClient';
import { useToast } from '@/hooks/ui/use-toasts';
import { Amplify } from 'aws-amplify';
import { post } from 'aws-amplify/api';
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

Amplify.configure(outputs);
const existingConfig = Amplify.getConfig();
Amplify.configure({
...existingConfig,
API: {
...existingConfig.API,
REST: outputs.custom.APIs,
},
});

interface PricingCardProps {
plan: {
Expand All @@ -36,22 +17,8 @@ interface PricingCardProps {
};
}

interface SubscriptionResponse {
checkoutUrl?: string;
error?: string;
details?: string;
}

export function PricingCard({ plan }: PricingCardProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const isClient = useIsClient();
const { user, loading } = useAuth();
const { toasts, addToast, removeToast } = useToast();
const router = useRouter();

const cognitoUsername = user?.userId;
const hasActivePlan = user && user.plan ? user.plan === plan.name : false;

const formatPrice = (price: string) => {
if (price === '0') return 'Gratis';
const numPrice = Number.parseInt(price, 10);
Expand All @@ -63,118 +30,52 @@ export function PricingCard({ plan }: PricingCardProps) {
return `$ ${formattedPrice}`;
};

const handleSubscribe = async () => {
if (!user) {
router.push('/login');
return;
}

setIsSubmitting(true);
try {
const restOperation = post({
apiName: 'SubscriptionApi',
path: 'subscribe',
options: {
body: {
userId: cognitoUsername || '',
email: user.email,
name: user.nickName || '',
plan: {
polarId: plan.polarId,
},
},
},
});

const { body } = await restOperation.response;
const response = (await body.json()) as SubscriptionResponse;

if (response && response.checkoutUrl) {
window.location.href = response.checkoutUrl;
} else {
throw new Error('Url not found');
}
} catch (error) {
console.error('Error processing subscription:', error);
addToast('Hubo un error al procesar tu suscripción. Por favor, inténtalo de nuevo.', 'error');
setIsSubmitting(false);
}
const handleSubscribe = () => {
router.push('/my-store');
};

useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden && isSubmitting) {
setIsSubmitting(false);
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [isSubmitting]);

if (!isClient || loading) {
return <LoadingIndicator text="Cargando..." />;
}

return (
<>
{isSubmitting && (
<div className="fixed inset-0 flex items-center justify-center bg-white/30 backdrop-blur-md z-50">
<LoadingIndicator text="Procesando suscripción..." />
<motion.div
className="relative rounded-2xl bg-white shadow-lg border border-gray-200"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}>
{plan.popular && (
<div className="absolute -top-3 left-0 right-0">
<div className="mx-auto w-fit rounded-full bg-primary px-4 py-1 text-center text-sm font-medium text-white shadow-md">
Más popular
</div>
</div>
)}

<motion.div
className="relative rounded-2xl bg-white shadow-lg border border-gray-200"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}>
{plan.popular && (
<div className="absolute -top-3 left-0 right-0">
<div className="mx-auto w-fit rounded-full bg-primary px-4 py-1 text-center text-sm font-medium text-white shadow-md">
Más popular
</div>
</div>
)}
<div className="p-8 flex flex-col h-full justify-between">
<div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">{plan.name}</h3>
<p className="text-sm text-gray-600 mb-6">{plan.description}</p>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900">{formatPrice(plan.price)}</span>
<span className="ml-2 text-sm text-gray-600">COP al mes</span>
<p className="mt-1 text-sm text-gray-500">facturación mensual</p>
</div>
</div>

<div className="mb-8">
<h4 className="font-semibold text-gray-900 mb-4">Funciones destacadas</h4>
<ul className="space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start">
<Check className="mr-3 h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
<span className="text-sm text-gray-600">{feature}</span>
</li>
))}
</ul>
<div className="p-8 flex flex-col h-full justify-between">
<div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">{plan.name}</h3>
<p className="text-sm text-gray-600 mb-6">{plan.description}</p>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900">{formatPrice(plan.price)}</span>
<span className="ml-2 text-sm text-gray-600">COP al mes</span>
<p className="mt-1 text-sm text-gray-500">facturación mensual</p>
</div>
</div>

<Button
className={`w-full rounded-full px-6 py-3 text-sm font-medium transition-colors duration-300 ${
hasActivePlan
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-primary text-white hover:bg-primary-dark'
}`}
onClick={handleSubscribe}
disabled={isSubmitting || hasActivePlan}>
{hasActivePlan ? 'Plan activo' : isSubmitting ? 'Procesando...' : plan.buttonText}
</Button>
<div className="mb-8">
<h4 className="font-semibold text-gray-900 mb-4">Funciones destacadas</h4>
<ul className="space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start">
<Check className="mr-3 h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
<span className="text-sm text-gray-600">{feature}</span>
</li>
))}
</ul>
</div>
</motion.div>

<Toast toasts={toasts} removeToast={removeToast} />
</>
<Button
className={`w-full rounded-full px-6 py-3 text-sm font-medium transition-colors duration-300 bg-primary text-white hover:bg-primary-dark`}
onClick={handleSubscribe}>
{plan.buttonText}
</Button>
</div>
</motion.div>
);
}
12 changes: 0 additions & 12 deletions app/(www)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
'use client';

import outputs from '@/amplify_outputs.json';
import { Footer } from '@/app/(www)/landing/components/Footer';
import { faqItems } from '@/app/(www)/pricing/components/FAQItem';
import { FAQSection } from '@/app/(www)/pricing/components/FAQSection';
import { FeatureComparison } from '@/app/(www)/pricing/components/FeatureComparison';
import { plans } from '@/app/(www)/pricing/components/plans';
import { PricingCard } from '@/app/(www)/pricing/components/PricingCard';
import { Amplify } from 'aws-amplify';
import { motion } from 'framer-motion';

Amplify.configure(outputs);
const existingConfig = Amplify.getConfig();
Amplify.configure({
...existingConfig,
API: {
...existingConfig.API,
REST: outputs.custom.APIs,
},
});

export default function PricingPage() {
return (
<>
Expand Down
81 changes: 81 additions & 0 deletions app/store/[slug]/access_account/checkout/CheckoutModalClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

/*
* Copyright 2025 Fasttify LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { CheckoutModal } from '@/app/store/components/checkout-modal/CheckoutModal';
import { CheckoutModalMobile } from '@/app/store/components/checkout-modal/CheckoutModalMobile';
import { useEffect } from 'react';
import useAuthStore from '@/context/core/userStore';

/**
* Componente cliente que maneja el modal de checkout bloqueante
* Previene la navegación y aplica efectos visuales al fondo
*/
export function CheckoutModalClient({ storeId: _storeId }: { storeId: string }) {
const { logout } = useAuthStore();
/**
* Previene la navegación del usuario mientras está en el modal de checkout
*/
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = '';
};

const handlePopState = () => {
// Prevenir navegación hacia atrás
window.history.pushState(null, '', window.location.href);
};

// Agregar el estado actual al historial para prevenir navegación
window.history.pushState(null, '', window.location.href);

// Agregar listeners para prevenir navegación
window.addEventListener('beforeunload', handleBeforeUnload);
window.addEventListener('popstate', handlePopState);

// Bloquear el scroll del body
document.body.style.overflow = 'hidden';

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
window.removeEventListener('popstate', handlePopState);
document.body.style.overflow = 'unset';
};
}, []);

/**
* Maneja el cierre de sesión y redirige al login
*/
const handleLogout = () => {
logout();
};

return (
<>
{/* Modal para desktop */}
<div className="hidden lg:block">
<CheckoutModal open={true} onClose={handleLogout} />
</div>

{/* Modal para móvil */}
<div className="lg:hidden">
<CheckoutModalMobile open={true} onClose={handleLogout} />
</div>
</>
);
}
Loading