diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..64f0d4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Frontend Development +- `npm run dev` - Start Vite development server +- `npm run build` - Build for production +- `npm run build:ssr` - Build with SSR support +- `npm run lint` - Run ESLint with auto-fix +- `npm run types` - Run TypeScript type checking +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +### Backend Development +- `composer dev` - Start full development environment (Laravel server, queue worker, logs, Vite) +- `composer dev:ssr` - Start development with SSR +- `composer test` - Run PHP tests with Pest +- `php artisan serve` - Start Laravel development server +- `php artisan queue:listen --tries=1` - Start queue worker +- `php artisan pail --timeout=0` - Start log viewer + +## Architecture Overview + +### Stack +- **Backend**: Laravel 12 with PHP 8.2+ +- **Frontend**: React 19 + TypeScript with Inertia.js +- **Database**: PostgreSQL with pgvector extension for embeddings +- **Queue System**: Laravel Horizon for job processing +- **Payment**: Laravel Cashier with Stripe integration +- **Media Processing**: FFmpeg integration via pbmedia/laravel-ffmpeg +- **Authentication**: Laravel Sanctum + Google OAuth via Socialite + +### Core Application Structure + +**MemeAI Generator Platform**: This is a meme generation platform that uses AI to create memes from user inputs. + +#### Key Models & Relationships +- `User` - Uses Billable trait for Stripe, has UUID-based public IDs +- `Meme` - Generated memes with keyword categorization (action/emotion/misc) +- `MemeMedia` - Template media files for meme generation +- `BackgroundMedia` - Background images/videos for memes +- `Category` - Hierarchical categories using kalnoy/nestedset +- `UserUsage` & `UserPlan` - Usage tracking and subscription management +- `KeywordEmbedding` & `MemeMediaEmbedding` - Vector embeddings for content matching + +#### AI Integration Points +- `CloudflareAI`, `OpenAI`, `RunwareAI` - Multiple AI service integrations +- `MemeGenerator` - Core meme generation logic with keyword matching via vector similarity +- Uses pgvector for semantic search and content matching + +#### Frontend Architecture +- **Inertia.js** full-stack integration between Laravel and React +- **Zustand** for state management (VideoEditorStore, MediaStore, UserStore, PricingStore) +- **Radix UI** + **Tailwind CSS** for component library +- **Konva.js** + **React-Konva** for canvas-based video/meme editing +- **FFmpeg.wasm** for client-side media processing + +#### Media Processing Pipeline +- Video templates stored as JSON configurations +- Canvas-based editor with timeline support +- Server-side FFmpeg processing for final video generation +- S3/R2 storage integration via Laravel Filesystem + +#### Key Helper Classes +- `MediaEngine` - Media processing utilities +- `PurchaseHelper`, `SubscriptionHelper` - Payment processing +- `WatermarkUsageHelper` - Usage tracking and limits +- `StripeHelper` - Stripe integration utilities + +### Development Notes + +#### Testing +- Uses Pest PHP for backend testing +- Test files in `tests/Feature/` and `tests/Unit/` +- Run tests with `composer test` + +#### Code Organization +- Custom helpers in `app/Helpers/FirstParty/` +- Global helper functions in `app/Helpers/Global/helpers.php` +- Frontend components organized by feature in `resources/js/modules/` +- Reusable UI components in `resources/js/components/ui/` + +#### Special Configurations +- CORS headers configured for FFmpeg.wasm in Vite config +- Vector search capabilities via PostgreSQL pgvector extension +- Hashids for public ID generation (users, etc.) +- Queue-based background processing for media generation + +#### Environment-Specific Features +- Test routes can be enabled via `ENABLE_TEST_ROUTES=true` +- Uses Laravel Horizon for queue management in production +- Supports both regular and SSR deployments \ No newline at end of file diff --git a/MEMEAIGEN/STRIPE WEBHOOKS.bru b/MEMEAIGEN/STRIPE WEBHOOKS.bru index 94474e1..ee323d2 100644 --- a/MEMEAIGEN/STRIPE WEBHOOKS.bru +++ b/MEMEAIGEN/STRIPE WEBHOOKS.bru @@ -12,135 +12,136 @@ post { body:json { { - "id": "evt_1Rg71IEEXQJo9EEO4GHqZdRZ", + "id": "evt_1Rg71IEEXQJo9EEOmxJtfwVm", "object": "event", "api_version": "2025-05-28.basil", "created": 1751387215, "data": { "object": { - "id": "sub_1Rg71FEEXQJo9EEO18sXSEho", - "object": "subscription", + "id": "in_1Rg71FEEXQJo9EEOmxbGjtdH", + "object": "invoice", + "account_country": "MY", + "account_name": "MEMEAIGEN sandbox", + "account_tax_ids": null, + "amount_due": 400, + "amount_overpaid": 0, + "amount_paid": 400, + "amount_remaining": 0, + "amount_shipping": 0, "application": null, - "application_fee_percent": null, + "attempt_count": 1, + "attempted": true, + "auto_advance": false, "automatic_tax": { "disabled_reason": null, "enabled": false, - "liability": null - }, - "billing_cycle_anchor": 1751387213, - "billing_cycle_anchor_config": null, - "billing_mode": { - "type": "classic" - }, - "billing_thresholds": null, - "cancel_at": null, - "cancel_at_period_end": false, - "canceled_at": null, - "cancellation_details": { - "comment": null, - "feedback": null, - "reason": null + "liability": null, + "provider": null, + "status": null }, + "automatically_finalizes_at": null, + "billing_reason": "subscription_create", "collection_method": "charge_automatically", "created": 1751387213, "currency": "usd", + "custom_fields": null, "customer": "cus_SbGYl34MpG4nv5", - "days_until_due": null, - "default_payment_method": "pm_1Rg71EEEXQJo9EEOJWUAU6EQ", + "customer_address": null, + "customer_email": "memeaigen.com@gmail.com", + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, "default_source": null, "default_tax_rates": [], "description": null, "discounts": [], - "ended_at": null, - "invoice_settings": { - "account_tax_ids": null, - "issuer": { - "type": "self" - } + "due_date": null, + "effective_at": 1751387213, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1RfMzuEEXQJo9EEO/test_YWNjdF8xUmZNenVFRVhRSm85RUVPLF9TYkplU0VDWWdKSG53bDVZT2VuNjNGZFFaaFJsbFFkLDE0MTkyODAxNg0200EQHRg6hn?s=ap", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1RfMzuEEXQJo9EEO/test_YWNjdF8xUmZNenVFRVhRSm85RUVPLF9TYkplU0VDWWdKSG53bDVZT2VuNjNGZFFaaFJsbFFkLDE0MTkyODAxNg0200EQHRg6hn/pdf?s=ap", + "issuer": { + "type": "self" }, - "items": { + "last_finalization_error": null, + "latest_revision": null, + "lines": { "object": "list", "data": [ { - "id": "si_SbJesuW5WgGoZ7", - "object": "subscription_item", - "billing_thresholds": null, - "created": 1751387213, - "current_period_end": 1754065613, - "current_period_start": 1751387213, + "id": "il_1Rg71FEEXQJo9EEOhVjRevMb", + "object": "line_item", + "amount": 400, + "currency": "usd", + "description": "1 × Personal Creator (at $4.00 / month)", + "discount_amounts": [], + "discountable": true, "discounts": [], - "metadata": {}, - "plan": { - "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", - "object": "plan", - "active": true, - "amount": 400, - "amount_decimal": "400", - "billing_scheme": "per_unit", - "created": 1751210467, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "meter": null, - "nickname": null, - "product": "prod_SaY8TGjiPi5hWu", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" + "invoice": "in_1Rg71FEEXQJo9EEOmxbGjtdH", + "livemode": false, + "metadata": { + "is_on_session_checkout": "true" }, - "price": { - "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1751210467, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": {}, - "nickname": null, - "product": "prod_SaY8TGjiPi5hWu", - "recurring": { - "interval": "month", - "interval_count": 1, - "meter": null, - "trial_period_days": null, - "usage_type": "licensed" + "parent": { + "invoice_item_details": null, + "subscription_item_details": { + "invoice_item": null, + "proration": false, + "proration_details": { + "credited_items": null + }, + "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho", + "subscription_item": "si_SbJesuW5WgGoZ7" }, - "tax_behavior": "unspecified", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 400, + "type": "subscription_item_details" + }, + "period": { + "end": 1754065613, + "start": 1751387213 + }, + "pretax_credit_amounts": [], + "pricing": { + "price_details": { + "price": "price_1RfN2VEEXQJo9EEOzjPI2HGt", + "product": "prod_SaY8TGjiPi5hWu" + }, + "type": "price_details", "unit_amount_decimal": "400" }, "quantity": 1, - "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho", - "tax_rates": [] + "taxes": [] } ], "has_more": false, "total_count": 1, - "url": "/v1/subscription_items?subscription=sub_1Rg71FEEXQJo9EEO18sXSEho" + "url": "/v1/invoices/in_1Rg71FEEXQJo9EEOmxbGjtdH/lines" }, - "latest_invoice": "in_1Rg71FEEXQJo9EEOmxbGjtdH", "livemode": false, - "metadata": { - "is_on_session_checkout": "true" - }, - "next_pending_invoice_item_invoice": null, + "metadata": {}, + "next_payment_attempt": null, + "number": "IN7OJLTH-0001", "on_behalf_of": null, - "pause_collection": null, + "parent": { + "quote_details": null, + "subscription_details": { + "metadata": { + "is_on_session_checkout": "true" + }, + "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho" + }, + "type": "subscription_details" + }, "payment_settings": { + "default_mandate": null, "payment_method_options": { "acss_debit": null, "bancontact": null, "card": { - "network": null, "request_three_d_secure": "automatic" }, "customer_balance": null, @@ -148,50 +149,34 @@ body:json { "sepa_debit": null, "us_bank_account": null }, - "payment_method_types": null, - "save_default_payment_method": "off" + "payment_method_types": null }, - "pending_invoice_item_interval": null, - "pending_setup_intent": null, - "pending_update": null, - "plan": { - "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt", - "object": "plan", - "active": true, - "amount": 400, - "amount_decimal": "400", - "billing_scheme": "per_unit", - "created": 1751210467, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": {}, - "meter": null, - "nickname": null, - "product": "prod_SaY8TGjiPi5hWu", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" + "period_end": 1751387213, + "period_start": 1751387213, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "receipt_number": null, + "rendering": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1751387213, + "marked_uncollectible_at": null, + "paid_at": 1751387215, + "voided_at": null }, - "quantity": 1, - "schedule": null, - "start_date": 1751387213, - "status": "active", + "subtotal": 400, + "subtotal_excluding_tax": 400, "test_clock": null, - "transfer_data": null, - "trial_end": null, - "trial_settings": { - "end_behavior": { - "missing_payment_method": "create_invoice" - } - }, - "trial_start": null - }, - "previous_attributes": { - "default_payment_method": null, - "status": "incomplete" + "total": 400, + "total_discount_amounts": [], + "total_excluding_tax": 400, + "total_pretax_credit_amounts": [], + "total_taxes": [], + "webhooks_delivered_at": null } }, "livemode": false, @@ -200,6 +185,6 @@ body:json { "id": null, "idempotency_key": "e50baede-20b6-4a06-a2c4-15f43ca47dd4" }, - "type": "customer.subscription.updated" + "type": "invoice.paid" } } diff --git a/app/Helpers/FirstParty/Purchase/PurchaseHelper.php b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php index 6fed130..74043c1 100644 --- a/app/Helpers/FirstParty/Purchase/PurchaseHelper.php +++ b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php @@ -43,7 +43,6 @@ public static function getSubscriptionPlanByStripePriceID($stripe_price_id) foreach ($plans as $plan) { - if ($plan['id'] == 'free') { continue; } @@ -76,10 +75,10 @@ public static function getPlanSystemProperty($plan, $property) // Inject environment into the path // stripe.product_id.month becomes system.stripe.product_id.{env}.month array_splice($propertyParts, 2, 0, $environment); - $fullPath = 'system.' . implode('.', $propertyParts); + $fullPath = 'system.'.implode('.', $propertyParts); } else { // For non-stripe properties, just prepend 'system.' - $fullPath = 'system.' . $property; + $fullPath = 'system.'.$property; } return data_get($plan, $fullPath); diff --git a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php index 379b2a8..1a04588 100644 --- a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php +++ b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php @@ -5,7 +5,6 @@ use App\Helpers\FirstParty\Stripe\StripeHelper; use App\Models\Plan; use App\Models\UserPlan; -use Illuminate\Support\Facades\Log; use Laravel\Cashier\Events\WebhookReceived; class SubscriptionHelper @@ -28,7 +27,7 @@ private static function handleSubscriptionUpsert(WebhookReceived $event) { $object = $event->payload['data']['object']; - //dump($object); + // dump($object); $ignore_statuses = ['incomplete_expired', 'incomplete']; @@ -41,7 +40,7 @@ private static function handleSubscriptionUpsert(WebhookReceived $event) if ($user) { foreach ($object['items']['data'] as $line_item) { - //dump($line_item); + // dump($line_item); $stripe_price_id = $line_item['plan']['id']; $current_period_end = $line_item['current_period_end']; diff --git a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php index 617cbb5..ae1d5ec 100644 --- a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php +++ b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php @@ -4,7 +4,6 @@ use App\Helpers\FirstParty\Stripe\StripeHelper; use App\Models\UserUsage; -use Illuminate\Support\Facades\App; use Laravel\Cashier\Events\WebhookReceived; class WatermarkUsageHelper @@ -22,11 +21,11 @@ private static function handleInvoicePaid(WebhookReceived $event) { $object = $event->payload['data']['object']; - //dump($object); + // dump($object); $accept_statuses = ['paid', 'partially_paid']; - if (!in_array($object['status'], $accept_statuses)) { + if (! in_array($object['status'], $accept_statuses)) { return; } @@ -36,7 +35,7 @@ private static function handleInvoicePaid(WebhookReceived $event) foreach ($object['lines']['data'] as $line_item) { $stripe_price_id = $line_item['pricing']['price_details']['price']; - //dd($stripe_price_id); + // dd($stripe_price_id); $subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id); @@ -57,7 +56,7 @@ private static function handleInvoicePaid(WebhookReceived $event) } } - //dd($subscription_config); + // dd($subscription_config); } } } diff --git a/app/Helpers/FirstParty/Stripe/StripeHelper.php b/app/Helpers/FirstParty/Stripe/StripeHelper.php index 6e63b69..b8d1ccd 100644 --- a/app/Helpers/FirstParty/Stripe/StripeHelper.php +++ b/app/Helpers/FirstParty/Stripe/StripeHelper.php @@ -3,7 +3,6 @@ namespace App\Helpers\FirstParty\Stripe; use App\Models\User; -use Laravel\Cashier\Events\WebhookReceived; class StripeHelper { diff --git a/app/Http/Controllers/SocialAuthController.php b/app/Http/Controllers/SocialAuthController.php index aca487b..1953636 100644 --- a/app/Http/Controllers/SocialAuthController.php +++ b/app/Http/Controllers/SocialAuthController.php @@ -65,7 +65,7 @@ public function handleGoogleCallback() return redirect()->intended(route('home'))->with('success', "You're now logged in!"); } catch (\Exception $e) { - //throw $e; + // throw $e; $error_message = 'Google login failed. Please try again.'; if (config('app.debug')) { $error_message = $e->getMessage(); diff --git a/app/Http/Controllers/UserAccountController.php b/app/Http/Controllers/UserAccountController.php new file mode 100644 index 0000000..ef0aad8 --- /dev/null +++ b/app/Http/Controllers/UserAccountController.php @@ -0,0 +1,28 @@ +load('user_usage'); + $user->load('plan'); + + return response()->json([ + 'success' => [ + 'data' => [ + 'user' => $user, + 'billing' => [ + 'provider' => 'stripe', + 'portal' => Auth::user()->billingPortalUrl(route('home')) + ] + ], + ], + ]); + } +} diff --git a/app/Models/Plan.php b/app/Models/Plan.php index 02b1f15..beaa3c5 100644 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -26,4 +26,11 @@ class Plan extends Model 'name', 'tier', ]; + + protected $hidden = [ + 'id', + 'created_at', + 'updated_at', + 'laravel_through_key', + ]; } diff --git a/app/Models/User.php b/app/Models/User.php index 4e43720..2c794dd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,8 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; + +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -14,7 +16,7 @@ class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable, SoftDeletes, Billable; + use Billable, HasApiTokens, HasFactory, Notifiable, SoftDeletes; /** * The attributes that are mass assignable. @@ -37,8 +39,28 @@ class User extends Authenticatable 'password', 'remember_token', 'id', + 'created_at', + 'updated_at', + 'deleted_at', + 'uuid', + 'stripe_id', + 'google_id', + 'email_verified_at', + 'pm_last_four', + 'pm_type', + 'trial_ends_at', + 'email', ]; + protected $appends = ['ids']; + + protected function ids(): Attribute + { + return Attribute::make( + get: fn($value, $attributes) => hashids_encode($attributes['id']), + ); + } + /** * Get the attributes that should be cast. * @@ -61,4 +83,19 @@ protected static function booted(): void $model->uuid = $model->uuid ?? (string) Str::uuid(); }); } + + public function user_usage() + { + return $this->hasOne(UserUsage::class, 'user_id'); + } + + public function user_plan() + { + return $this->hasOne(UserPlan::class, 'user_id'); + } + + public function plan() + { + return $this->hasOneThrough(Plan::class, UserPlan::class, 'user_id', 'id', 'id', 'plan_id'); + } } diff --git a/app/Models/UserPlan.php b/app/Models/UserPlan.php index 7b3e150..cdf8cc6 100644 --- a/app/Models/UserPlan.php +++ b/app/Models/UserPlan.php @@ -40,4 +40,14 @@ class UserPlan extends Model 'cancel_at', 'canceled_at', ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function plan() + { + return $this->belongsTo(Plan::class, 'plan_id'); + } } diff --git a/app/Models/UserUsage.php b/app/Models/UserUsage.php index 171dabe..6ea6d7d 100644 --- a/app/Models/UserUsage.php +++ b/app/Models/UserUsage.php @@ -7,30 +7,45 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; /** * Class UserUsage - * + * * @property int $id * @property int $user_id * @property int $non_watermark_videos_left * @property Carbon|null $created_at * @property Carbon|null $updated_at - * - * @package App\Models */ class UserUsage extends Model { - protected $table = 'user_usages'; + protected $table = 'user_usages'; - protected $casts = [ - 'user_id' => 'int', - 'non_watermark_videos_left' => 'int' - ]; + protected $casts = [ + 'user_id' => 'int', + 'non_watermark_videos_left' => 'int', + ]; - protected $fillable = [ - 'user_id', - 'non_watermark_videos_left' - ]; + protected $fillable = [ + 'user_id', + 'non_watermark_videos_left', + ]; + + protected $hidden = [ + 'created_at', + 'updated_at', + 'id', + 'user_id', + ]; + + protected $appends = ['ids']; + + protected function ids(): Attribute + { + return Attribute::make( + get: fn($value, $attributes) => hashids_encode($attributes['id']), + ); + } } diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 17de309..5c06599 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -38,6 +38,7 @@ createInertiaApp({ + diff --git a/resources/js/modules/auth/auth-user.jsx b/resources/js/modules/auth/auth-user.jsx new file mode 100644 index 0000000..926118d --- /dev/null +++ b/resources/js/modules/auth/auth-user.jsx @@ -0,0 +1,18 @@ +import useUserStore from '@/stores/UserStore'; +import { usePage } from '@inertiajs/react'; +import { useEffect } from 'react'; + +const AuthUser = () => { + const { auth } = usePage().props; + const { user, fetchUser } = useUserStore(); + + useEffect(() => { + if (auth.user) { + if (user == null) { + fetchUser(); + } + } + }, [auth.user]); +}; + +export default AuthUser; diff --git a/resources/js/modules/upgrade/upgrade-sheet.jsx b/resources/js/modules/upgrade/upgrade-sheet.jsx index a2e4774..3e5862a 100644 --- a/resources/js/modules/upgrade/upgrade-sheet.jsx +++ b/resources/js/modules/upgrade/upgrade-sheet.jsx @@ -1,4 +1,6 @@ // resources/js/Pages/User/Partials/upgrade-sheet.jsx +import { usePage } from '@inertiajs/react'; + import { SparklesText } from '@/components/magicui/sparkles-text'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Button } from '@/components/ui/button'; @@ -8,11 +10,17 @@ 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 } = usePricingStore(); + const { plan, billing, user_usage } = useUserStore(); + const { auth } = usePage().props; + + const [isRedirectingToBilling, setIsRedirectingToBilling] = useState(false); // State to control sheet visibility const [isOpen, setIsOpen] = useState(false); @@ -104,36 +112,108 @@ const UpgradeSheet = () => {
- {subscription ? ( -
- - Upgrade to {subscription?.name} Plan

at only {subscription?.symbol} - {subscription?.amount} - {subscription?.primary_interval === 'month' ? '/m' : '/y'}* -
- -
- -
* Launch pricing limited to first 1000 users
+ {auth?.user ? ( +
+
+ {/* Non-watermark Exports */} + {user_usage?.non_watermark_videos_left && ( +
+
+
+ + Non-watermark Exports +
+
{user_usage?.non_watermark_videos_left}
+
exports left
+
+ +
+ )} + + {/* Credits */} +
+
+
+ + Credits +
+
999
+
available
+
+ +
+ ) : ( +
+
Have an account with us?
+ +
+ )} + + {subscription ? ( + <> + {auth.user && plan.tier != 'free' ? ( +
+ You're now in the {plan?.name} plan! + + {billing?.portal && ( + + )} + +
+ + All our payments are securely processed by Stripe. +
+
+ ) : ( +
+ + Upgrade to {subscription?.name} Plan

at only {subscription?.symbol} + {subscription?.amount} + {subscription?.primary_interval === 'month' ? '/m' : '/y'}* +
+ +
+ +
* Launch pricing limited to first 1000 users
+ +
+ + All our payments are securely processed by Stripe. +
+
+
+ )} + ) : ( isFetchingPricing && (
diff --git a/resources/js/pages/home/home.tsx b/resources/js/pages/home/home.tsx index d0cd1d8..b760aff 100644 --- a/resources/js/pages/home/home.tsx +++ b/resources/js/pages/home/home.tsx @@ -1,3 +1,4 @@ +import AuthUser from '@/modules/auth/auth-user'; import Editor from '@/modules/editor/editor.jsx'; import FlashMessages from '@/modules/flash/flash-messages'; @@ -6,6 +7,7 @@ const Home = () => {
+
); }; diff --git a/resources/js/stores/UserStore.js b/resources/js/stores/UserStore.js new file mode 100644 index 0000000..5a5db68 --- /dev/null +++ b/resources/js/stores/UserStore.js @@ -0,0 +1,50 @@ +import axiosInstance from '@/plugins/axios-plugin'; +import { mountStoreDevtool } from 'simple-zustand-devtools'; +import { route } from 'ziggy-js'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +const useUserStore = create( + devtools((set, get) => ({ + // Pricing Plans + user: null, + user_usage: null, + plan: { + name: 'Free', + tier: 'free', + }, + billing: null, + + isLoadingUser: false, + + // Fetch backgrounds + fetchUser: async () => { + set({ isLoadingUser: true }); + try { + const response = await axiosInstance.post(route('api.user')); + set({ + user: response.data.success.data.user, + user_usage: response.data.success.data.user.user_usage, + plan: response.data.success.data.user.plan, + billing: response.data.success.data.billing, + }); + //return response.data.success.data; + } catch (error) { + console.error(route('api.user')); + console.error('Error fetching:', error); + } finally { + set({ isLoadingUser: false }); + } + }, + })), + { + name: 'UserStore', + store: 'UserStore', + }, +); + +if (import.meta.env.APP_ENV === 'local') { + mountStoreDevtool('UserStore', useUserStore); +} + +export default useUserStore; diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js index a2f9b69..2ca9a26 100644 --- a/resources/js/ziggy.js +++ b/resources/js/ziggy.js @@ -1,4 +1,4 @@ -const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"cashier.payment":{"uri":"stripe\/payment\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"cashier.webhook":{"uri":"stripe\/webhook","methods":["POST"]},"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.pricing_page":{"uri":"api\/pricing","methods":["POST"]},"api.user.subscribe":{"uri":"api\/user\/subscribe","methods":["POST"]},"api.user.purchase":{"uri":"api\/user\/purchase","methods":["POST"]},"api.app.init":{"uri":"api\/app\/init","methods":["POST"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"auth.google.redirect":{"uri":"auth\/google\/redirect","methods":["GET","HEAD"]},"auth.google.callback":{"uri":"auth\/google\/callback","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"subscribe.success":{"uri":"subscribe\/success","methods":["GET","HEAD"]},"subscribe.cancelled":{"uri":"subscribe\/cancelled","methods":["GET","HEAD"]},"purchase.success":{"uri":"purchase\/success","methods":["GET","HEAD"]},"purchase.cancelled":{"uri":"purchase\/cancelled","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"admin.background-generation":{"uri":"admin\/background-generation","methods":["GET","HEAD"]},"admin.background-generation.generate":{"uri":"admin\/background-generation\/generate","methods":["POST"]},"admin.background-generation.save":{"uri":"admin\/background-generation\/save","methods":["POST"]},"admin.background-generation.delete":{"uri":"admin\/background-generation\/delete\/{id}","methods":["POST"],"parameters":["id"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; +const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"cashier.payment":{"uri":"stripe\/payment\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"cashier.webhook":{"uri":"stripe\/webhook","methods":["POST"]},"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.pricing_page":{"uri":"api\/pricing","methods":["POST"]},"api.user":{"uri":"api\/user","methods":["POST"]},"api.user.subscribe":{"uri":"api\/user\/subscribe","methods":["POST"]},"api.user.purchase":{"uri":"api\/user\/purchase","methods":["POST"]},"api.app.init":{"uri":"api\/app\/init","methods":["POST"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"auth.google.redirect":{"uri":"auth\/google\/redirect","methods":["GET","HEAD"]},"auth.google.callback":{"uri":"auth\/google\/callback","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"subscribe.success":{"uri":"subscribe\/success","methods":["GET","HEAD"]},"subscribe.cancelled":{"uri":"subscribe\/cancelled","methods":["GET","HEAD"]},"purchase.success":{"uri":"purchase\/success","methods":["GET","HEAD"]},"purchase.cancelled":{"uri":"purchase\/cancelled","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"admin.background-generation":{"uri":"admin\/background-generation","methods":["GET","HEAD"]},"admin.background-generation.generate":{"uri":"admin\/background-generation\/generate","methods":["POST"]},"admin.background-generation.save":{"uri":"admin\/background-generation\/save","methods":["POST"]},"admin.background-generation.delete":{"uri":"admin\/background-generation\/delete\/{id}","methods":["POST"],"parameters":["id"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { Object.assign(Ziggy.routes, window.Ziggy.routes); } diff --git a/routes/api.php b/routes/api.php index 1209ea6..b268d4c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,8 +2,8 @@ use App\Http\Controllers\Auth\SanctumAuthController; use App\Http\Controllers\FrontMediaController; +use App\Http\Controllers\UserAccountController; use App\Http\Controllers\UserPurchaseController; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::group(['prefix' => 'user'], function () { @@ -16,9 +16,8 @@ Route::middleware('auth:sanctum')->group(function () { Route::group(['prefix' => 'user'], function () { - Route::get('/', function (Request $request) { - return $request->user(); - }); + + Route::post('/', [UserAccountController::class, 'index'])->name('api.user'); Route::post('/subscribe', [UserPurchaseController::class, 'subscribe'])->name('api.user.subscribe'); diff --git a/routes/web.php b/routes/web.php index ebd9575..09049a5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -57,8 +57,8 @@ }); }); - require __DIR__.'/settings.php'; - require __DIR__.'/auth.php'; + require __DIR__ . '/settings.php'; + require __DIR__ . '/auth.php'; } -Route::get('/', [FrontHomeController::class, 'index'])->name('home')->middleware('cacheResponse:3600'); +Route::get('/', [FrontHomeController::class, 'index'])->name('home');