Files
memefast/resources/js/modules/upgrade/upgrade-sheet.jsx
2025-07-15 03:27:39 +08:00

297 lines
17 KiB
JavaScript

// resources/js/Pages/User/Partials/upgrade-sheet.jsx
import { SparklesText } from '@/components/magicui/sparkles-text';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Spinner } from '@/components/ui/spinner.js';
import { useMitt } from '@/plugins/MittContext';
import CartIcon from '@/reusables/cart-icon.jsx';
import CoinIcon from '@/reusables/coin-icon.jsx';
import usePricingStore from '@/stores/PricingStore.js';
import useUserStore from '@/stores/UserStore.js';
import { Download, ShieldIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import UpgradePlanCarousel from './partials/upgrade-plan-carousel.tsx';
const UpgradeSheet = () => {
const { subscription, one_times, isFetchingPricing, fetchPricing, isCheckingOut, checkoutSubscribe, checkoutPurchase } = usePricingStore();
const { plan, billing, user_usage, credits, redirectBillingPortal, isRedirectingToBilling, user } = useUserStore();
// State to control sheet visibility
const [isOpen, setIsOpen] = useState(false);
// Get mitt emitter
const emitter = useMitt();
useEffect(() => {
fetchPricing();
}, []);
// FAQ data array
const faqData = [
{
q: "What's included in the $4/m Personal Creator Plan?",
a: 'This $4/m plan includes 50 non-watermark videos for export (8¢ per video), which is sufficient for every creator to post almost twice a day. <br><br>This plan also includes a Personal license, which allows you to use the app for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the perfect choice for you.',
},
{
q: 'Why are your plans extremely affordable for creators?',
a: "We're glad you think this way! As creators ourselves, we know the journey of creating and monetizing your channel is not easy.<br><br> We believe that creators deserve access to high quality videos, and we're committed to making it accessible to every creator.<br><br>If one day you've made it to the top, don't forget us! We'd love to hear your growth stories.",
},
{
q: 'Can I use the Personal Creator Plan for my business & commercial projects?',
a: "A hard NO.<br><br> The Personal Creator Plan is designed for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the right choice for you. The Personal Creator Plan is designed to empower creators, helping them to monetize their channels and grow their audience. <br><br>However, if you are a business, we recommend you to use the Business Creator Plan (coming soon), which contains a Business License that you can use for commercial projects.<br><br>Contact us if you have an urgent need for this plan, and we'll see what can be done to help you.",
},
{
q: 'How do I lock in my subscription price?',
a: "Simple! Just subscribe to the plan before the next price change. We can't guarantee that you'll be able to purchase the same plan at the current price that you've locked in. But we'll do our best to make sure you get the best value for your money.",
},
{
q: 'Is the $4/month pricing permanent?',
a: 'Yes, the $4/month launch pricing is a special offer for our first 1000 users. After 1000 users, the plan will be priced at a higher rate. Lock in your launch pricing by subscribing now!',
},
{
q: 'What can I do with credits?',
a: 'You can use credits to access AI features such as AI caption generation & AI background generation.',
},
// {
// q: 'What is the difference of purchasing credits from a subscription vs a one-time credit pack?',
// a: 'When you purchase credits from a subscription, (e.g. 500 Credits/m) you get monthly allowance of 500 credits. Unused credits will not be carry forwarded to the next month.<br><br>When you purchase credits from a credit pack, (e.g. 500 Credit Pack) the credits will not expire and you can use them anytime.',
// },
{
q: 'Can I cancel my subscription anytime?',
a: "Yes! You can cancel your subscription at any time. Your plan will remain active until the end of your current billing period, and you'll retain access to all premium features during that time. Just know that you might not be able to purchase the same plan at the current price that you've locked in.",
},
{
q: 'Do you offer refunds?',
a: 'Yes, we offer refunds for subscription plans. Credits are non-refundable once purchased as they do not expire.',
},
];
useEffect(() => {
// Listen for open modal event
const openModalListener = () => {
setIsOpen(true);
};
// Register listener for opening the modal
emitter.on('openUpgradeSheet', openModalListener);
// Cleanup
return () => {
emitter.off('openUpgradeSheet', openModalListener);
};
}, [emitter]);
// Handle sheet state changes
const handleOpenChange = (open) => {
setIsOpen(open);
// If sheet is closing, emit the close event
if (!open) {
emitter.emit('closeUpgradeSheet');
}
};
const handleSubscribe = (subscription) => {
checkoutSubscribe(subscription.stripe_monthly_price_id);
};
const handlePurchaseOneTime = (one_time) => {
checkoutPurchase(one_time.stripe_price_id);
};
const handleRedirectBillingPortal = () => {
redirectBillingPortal();
};
return (
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<SheetContent side="bottom" className=" max-h-[calc(100dvh)] gap-0! overflow-y-scroll pb-1">
<SheetHeader className="mb-0">
<SheetTitle className="flex items-center justify-center gap-2 sm:text-center">
<CartIcon className={'h-4 w-4'} /> Store
</SheetTitle>
<SheetDescription className="hidden"> </SheetDescription>
</SheetHeader>
<div className="mx-auto w-full max-w-[600px] space-y-3 px-4">
{user ? (
<div id="stats">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{/* Non-watermark Exports */}
{user_usage?.non_watermark_videos_left != null && (
<div className="bg-card relative overflow-hidden rounded-xl border p-6 shadow-sm">
<div className="relative z-10">
<div className="mb-2 flex items-center gap-2">
<Download className="text-primary h-5 w-5" />
<span className="text-muted-foreground text-xs font-medium">Non-watermark Exports</span>
</div>
<div className="text-foreground text-xl font-bold">{user_usage?.non_watermark_videos_left}</div>
<div className="text-muted-foreground text-sm">exports left</div>
</div>
<Download className="bg-muted absolute -top-2 -right-2 h-20 w-20 opacity-15" />
</div>
)}
{/* Credits */}
{credits != null && (
<div className="bg-card relative overflow-hidden rounded-xl border p-6 shadow-sm">
<div className="relative z-10">
<div className="mb-2 flex items-center gap-2">
<CoinIcon className="text-primary h-5 w-5" />
<span className="text-muted-foreground text-xs font-medium">Credits</span>
</div>
<div className="text-foreground text-xl font-bold">{credits}</div>
<div className="text-muted-foreground text-sm">available</div>
</div>
<CoinIcon className="bg-muted absolute -top-2 -right-2 h-20 w-20 opacity-15" />
</div>
)}
</div>
</div>
) : (
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="text-xs">Have an account with us?</div>
<Button onClick={() => emitter.emit('login')} size="sm" variant="outline">
Login Now
</Button>
</div>
)}
{subscription ? (
<>
{user && plan.tier != 'free' ? (
<div className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
<SparklesText className="text-xl font-bold sm:text-xl">You're now in the {plan?.name} plan!</SparklesText>
</div>
) : (
<div id="plan-purchase" className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
<SparklesText className="text-xl font-bold sm:text-2xl">
Upgrade to {subscription?.name} Plan<br></br> at only {subscription?.symbol}
{subscription?.amount}
{subscription?.primary_interval === 'month' ? '/m' : '/y'}*
</SparklesText>
<UpgradePlanCarousel />
<div className="space-y-2">
<Button
disabled={isCheckingOut}
onClick={() => {
handleSubscribe(subscription);
}}
size="lg"
className="mx-auto w-[220px] text-sm sm:text-base"
>
{isCheckingOut ? (
<Spinner className="text-muted h-4 w-4" />
) : (
<span>
Subscribe at ({subscription?.symbol}
{subscription?.amount}
{subscription?.primary_interval === 'month' ? '/m' : '/y'})*
</span>
)}
</Button>
<div className="text-muted-foreground text-xs">* Launch pricing limited to first 1000 users</div>
</div>
</div>
)}
</>
) : (
isFetchingPricing && (
<div className="mx-auto w-full">
<Spinner className="h-6 w-6" />
</div>
)
)}
<div id="credit-purchase" className="space-y-6 rounded-lg border p-4 text-center sm:p-7">
<div className="text-xl font-extrabold sm:text-2xl">Buy Credit Packs</div>
<div className="text-muted-foreground text-sm">
Unlock AI meme captions and backgrounds with credits. Perfect for overcoming creator's block and discovering new concepts.
</div>
<div className="space-y-2">
{one_times.map((one_time, index) => (
<div
key={one_time.id}
className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0 flex-1 space-y-1">
<div className="grid text-start">
<div className="text inline-flex items-center font-semibold">
<CoinIcon className="inline h-4 w-4 flex-shrink-0" />
<span className="ml-1">{one_time.name}</span>
</div>
<div className="text-muted-foreground text-xs font-semibold break-words">{one_time.description}</div>
</div>
<div className="flex items-center gap-1">
<div className="text-muted-foreground text-sm">
{one_time.symbol}
{one_time.amount} per pack
</div>
</div>
</div>
<Button
disabled={isCheckingOut}
onClick={() => {
handlePurchaseOneTime(one_time);
}}
className="w-full flex-shrink-0 sm:w-auto"
>
{isCheckingOut ? <Spinner className="text-muted h-4 w-4" /> : <span>Buy</span>}
</Button>
</div>
))}
</div>
</div>
<div className="flex flex-col justify-center space-y-2 rounded border p-3">
<div className="mx-auto">
<Button
variant="outline"
onClick={() => {
handleRedirectBillingPortal();
}}
disabled={isRedirectingToBilling}
>
{isRedirectingToBilling ? <Spinner className="text-muted-foreground h-4 w-4" /> : 'Manage Billing'}
</Button>
</div>
<div className="text-muted-foreground text-center text-xs">
<ShieldIcon className="mr-1 mb-1 inline h-4 w-4" />
All our billing & payments are securely processed by Stripe.
</div>
</div>
{!(user && plan.tier != 'free') && (
<div id="faq" className="space-y-4 rounded-lg border p-4 sm:p-7">
<div className="text-center text-xl font-extrabold sm:text-2xl">Frequently Asked Questions</div>
<Accordion type="single" collapsible className="w-full" defaultValue="item-1">
{faqData.map((faq, index) => (
<AccordionItem key={index} value={`item-${index + 1}`}>
<AccordionTrigger className="text-left text-sm break-words sm:text-base">{faq.q}</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<div className="overflow-wrap-anywhere text-sm break-words" dangerouslySetInnerHTML={{ __html: faq.a }} />
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)}
</div>
<SheetFooter>
<SheetClose asChild className="hidden">
<Button variant="outline">Close</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
export default UpgradeSheet;