From 79e7d7a49e717fc0dd5357a8790d95b731820fb3 Mon Sep 17 00:00:00 2001 From: ct Date: Tue, 1 Jul 2025 20:54:26 +0800 Subject: [PATCH] Update --- MEMEAIGEN/PRICING.bru | 11 + MEMEAIGEN/bruno.json | 9 + STRIPE.md | 3 + _ide_helper.php | 149 ++++ app/Helpers/FirstParty/AI/OpenAI.php | 39 +- .../FirstParty/Purchase/PurchaseHelper.php | 169 ++++ app/Http/Controllers/SocialAuthController.php | 93 +++ app/Http/Controllers/TestController.php | 13 +- .../Controllers/UserPurchaseController.php | 132 +++ bootstrap/app.php | 11 +- composer.json | 2 + composer.lock | 761 +++++++++++++++++- config/cashier.php | 127 +++ config/platform/general.php | 2 + config/platform/purchases/one_time.php | 37 + config/platform/purchases/subscriptions.php | 134 +++ config/services.php | 6 + ...01_074420_add_google_id_to_users_table.php | 28 + ...12905_set_password_null_to_users_table.php | 28 + ...5_07_01_114203_create_customer_columns.php | 40 + ...7_01_114204_create_subscriptions_table.php | 37 + ...114205_create_subscription_items_table.php | 34 + package-lock.json | 79 ++ package.json | 2 + resources/css/app.css | 19 +- resources/js/app.tsx | 4 + .../components/magicui/pulsating-button.tsx | 46 ++ .../js/components/magicui/sparkles-text.tsx | 150 ++++ resources/js/modules/auth/AuthDialog.jsx | 78 ++ resources/js/modules/editor/editor.jsx | 2 + .../editor/partials/edit-nav-sidebar.jsx | 51 +- .../modules/editor/partials/editor-header.jsx | 46 +- .../partials/upgrade-plan-carousel.tsx | 103 +++ .../js/modules/upgrade/upgrade-sheet.jsx | 198 +++++ resources/js/plugins/axios-plugin.jsx | 8 + resources/js/reusables/cart-icon.jsx | 48 ++ .../{coin-icon.tsx => coin-icon.jsx} | 8 +- resources/js/stores/PricingStore.js | 95 +++ resources/js/ziggy.js | 2 +- routes/api.php | 7 + routes/test.php | 2 + routes/web.php | 26 + 42 files changed, 2742 insertions(+), 97 deletions(-) create mode 100644 MEMEAIGEN/PRICING.bru create mode 100644 MEMEAIGEN/bruno.json create mode 100644 STRIPE.md create mode 100644 app/Helpers/FirstParty/Purchase/PurchaseHelper.php create mode 100644 app/Http/Controllers/SocialAuthController.php create mode 100644 app/Http/Controllers/UserPurchaseController.php create mode 100644 config/cashier.php create mode 100644 config/platform/purchases/one_time.php create mode 100644 config/platform/purchases/subscriptions.php create mode 100644 database/migrations/2025_07_01_074420_add_google_id_to_users_table.php create mode 100644 database/migrations/2025_07_01_112905_set_password_null_to_users_table.php create mode 100644 database/migrations/2025_07_01_114203_create_customer_columns.php create mode 100644 database/migrations/2025_07_01_114204_create_subscriptions_table.php create mode 100644 database/migrations/2025_07_01_114205_create_subscription_items_table.php create mode 100644 resources/js/components/magicui/pulsating-button.tsx create mode 100644 resources/js/components/magicui/sparkles-text.tsx create mode 100644 resources/js/modules/auth/AuthDialog.jsx create mode 100644 resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx create mode 100644 resources/js/modules/upgrade/upgrade-sheet.jsx create mode 100644 resources/js/reusables/cart-icon.jsx rename resources/js/reusables/{coin-icon.tsx => coin-icon.jsx} (95%) create mode 100644 resources/js/stores/PricingStore.js diff --git a/MEMEAIGEN/PRICING.bru b/MEMEAIGEN/PRICING.bru new file mode 100644 index 0000000..df339af --- /dev/null +++ b/MEMEAIGEN/PRICING.bru @@ -0,0 +1,11 @@ +meta { + name: PRICING + type: http + seq: 2 +} + +post { + url: https://memeaigen.test/api/pricing + body: none + auth: none +} diff --git a/MEMEAIGEN/bruno.json b/MEMEAIGEN/bruno.json new file mode 100644 index 0000000..79cf39a --- /dev/null +++ b/MEMEAIGEN/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "MEMEAIGEN", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/STRIPE.md b/STRIPE.md new file mode 100644 index 0000000..e62388b --- /dev/null +++ b/STRIPE.md @@ -0,0 +1,3 @@ +stripe login + +stripe listen --forward-to https://memeaigen.test/stripe/webhook diff --git a/_ide_helper.php b/_ide_helper.php index ef3a00e..92d0239 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -23885,6 +23885,154 @@ public static function setSiteName($name) } } +namespace Laravel\Socialite\Facades { + /** + * + * + * @method array getScopes() + * @method \Laravel\Socialite\Contracts\Provider scopes(array|string $scopes) + * @method \Laravel\Socialite\Contracts\Provider setScopes(array|string $scopes) + * @method \Laravel\Socialite\Contracts\Provider redirectUrl(string $url) + * @see \Laravel\Socialite\SocialiteManager + */ + class Socialite { + /** + * Get a driver instance. + * + * @param string $driver + * @return mixed + * @static + */ + public static function with($driver) + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->with($driver); + } + + /** + * Build an OAuth 2 provider instance. + * + * @param string $provider + * @param array $config + * @return \Laravel\Socialite\Two\AbstractProvider + * @static + */ + public static function buildProvider($provider, $config) + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->buildProvider($provider, $config); + } + + /** + * Format the server configuration. + * + * @param array $config + * @return array + * @static + */ + public static function formatConfig($config) + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->formatConfig($config); + } + + /** + * Forget all of the resolved driver instances. + * + * @return \Laravel\Socialite\SocialiteManager + * @static + */ + public static function forgetDrivers() + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->forgetDrivers(); + } + + /** + * Set the container instance used by the manager. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return \Laravel\Socialite\SocialiteManager + * @static + */ + public static function setContainer($container) + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->setContainer($container); + } + + /** + * Get the default driver name. + * + * @return string + * @throws \InvalidArgumentException + * @static + */ + public static function getDefaultDriver() + { + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->getDefaultDriver(); + } + + /** + * Get a driver instance. + * + * @param string|null $driver + * @return mixed + * @throws \InvalidArgumentException + * @static + */ + public static function driver($driver = null) + { + //Method inherited from \Illuminate\Support\Manager + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->driver($driver); + } + + /** + * Register a custom driver creator Closure. + * + * @param string $driver + * @param \Closure $callback + * @return \Laravel\Socialite\SocialiteManager + * @static + */ + public static function extend($driver, $callback) + { + //Method inherited from \Illuminate\Support\Manager + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->extend($driver, $callback); + } + + /** + * Get all of the created "drivers". + * + * @return array + * @static + */ + public static function getDrivers() + { + //Method inherited from \Illuminate\Support\Manager + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->getDrivers(); + } + + /** + * Get the container instance used by the manager. + * + * @return \Illuminate\Contracts\Container\Container + * @static + */ + public static function getContainer() + { + //Method inherited from \Illuminate\Support\Manager + /** @var \Laravel\Socialite\SocialiteManager $instance */ + return $instance->getContainer(); + } + + } + } + namespace ProtoneMedia\LaravelFFMpeg\Support { /** * @@ -29517,6 +29665,7 @@ class SEOMeta extends \Artesaos\SEOTools\Facades\SEOMeta {} class Twitter extends \Artesaos\SEOTools\Facades\TwitterCard {} class OpenGraph extends \Artesaos\SEOTools\Facades\OpenGraph {} class Horizon extends \Laravel\Horizon\Horizon {} + class Socialite extends \Laravel\Socialite\Facades\Socialite {} class FFMpeg extends \ProtoneMedia\LaravelFFMpeg\Support\FFMpeg {} class ResponseCache extends \Spatie\ResponseCache\Facades\ResponseCache {} class Hashids extends \Vinkla\Hashids\Facades\Hashids {} diff --git a/app/Helpers/FirstParty/AI/OpenAI.php b/app/Helpers/FirstParty/AI/OpenAI.php index f93c9ea..6364408 100644 --- a/app/Helpers/FirstParty/AI/OpenAI.php +++ b/app/Helpers/FirstParty/AI/OpenAI.php @@ -12,7 +12,7 @@ public static function getMemeKeywords(string $name, string $description) $response = Http::withHeaders([ 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $apiKey, + 'Authorization' => 'Bearer '.$apiKey, ])->post('https://api.openai.com/v1/responses', [ 'model' => 'gpt-4.1-nano', 'input' => [ @@ -56,70 +56,68 @@ public static function getSingleMemeGenerator($user_prompt) $captions = [ [ 'caption_description' => 'A humorous, funny one-liner meme caption that starts with "When you", describes a specific visual or situational moment, avoids vagueness, and ends with no punctuation', - 'caption_style' => 'Relatable one-liners starting with "When you"' + 'caption_style' => 'Relatable one-liners starting with "When you"', ], [ 'caption_description' => 'A POV meme that starts with "POV: ", clearly describes a specific scenario or feeling with high context, and ends with no punctuation', - 'caption_style' => 'POV-style captions (e.g., "POV: finally logging off after pretending to work for 3 hours")' + 'caption_style' => 'POV-style captions (e.g., "POV: finally logging off after pretending to work for 3 hours")', ], [ 'caption_description' => 'A humorous, funny one-liner meme caption that starts with "I", grounded in a relatable, situational experience with visual context, and ends with no punctuation', - 'caption_style' => 'Relatable one-liners starting with "I"' + 'caption_style' => 'Relatable one-liners starting with "I"', ], [ 'caption_description' => 'A humorous, funny one-liner meme caption that starts with "You", focused on a clear, specific reaction or moment, and ends with no punctuation', - 'caption_style' => 'Relatable one-liners starting with "You"' + 'caption_style' => 'Relatable one-liners starting with "You"', ], [ 'caption_description' => 'A humorous, funny one-liner meme caption with no punctuation that describes a vivid, realistic scenario or reaction in a clearly defined context', - 'caption_style' => 'Visual punchlines (e.g., "Wearing a suit and contemplating my life in the elevator")' + 'caption_style' => 'Visual punchlines (e.g., "Wearing a suit and contemplating my life in the elevator")', ], [ 'caption_description' => 'A juxtaposition-style one-liner meme caption expressing a contrast or contradiction in a witty, punchy way, often revealing irony or absurdity, and ends with no punctuation (e.g., "I want a salary without a job"). Start with "I" or "You" to create a sense of contrast', - 'caption_style' => 'Juxtaposition (e.g., "I want a salary without a job")' + 'caption_style' => 'Juxtaposition (e.g., "I want a salary without a job")', ], [ 'caption_description' => 'A meme caption that starts with "TIL: ", presents a short, punchy, and ironic realization about work or life that feels meme-worthy, clearly sets up a visual or exaggerated truth, and ends with no punctuation (e.g., "TIL: rent is just a subscription to be alive")', - 'caption_style' => 'TIL captions (e.g., "TIL: rent is just a subscription to be alive")' + 'caption_style' => 'TIL captions (e.g., "TIL: rent is just a subscription to be alive")', ], [ 'caption_description' => 'A meme caption that starts with "TL;DR: ", provides a blunt, dry, or brutally honest summary of a situation, ideally workplace- or life-related, and ends with no punctuation (e.g., "TL;DR: we had a meeting about having fewer meetings")', - 'caption_style' => 'TL;DR captions (e.g., "TL;DR: we had a meeting about having fewer meetings")' + 'caption_style' => 'TL;DR captions (e.g., "TL;DR: we had a meeting about having fewer meetings")', ], [ 'caption_description' => 'A meme caption that starts with "The moment you realize", sets up an unexpected or awkward realization in a relatable work or life setting, and ends with no punctuation', - 'caption_style' => 'The moment you realize... (e.g., "The moment you realize your boss joined the call 5 minutes ago")' + 'caption_style' => 'The moment you realize... (e.g., "The moment you realize your boss joined the call 5 minutes ago")', ], [ 'caption_description' => 'A meme caption that uses the "Nobody:" format to exaggerate a reaction or absurd behavior, often with a silent or empty setup followed by an over-the-top response, and ends with no punctuation', - 'caption_style' => 'Nobody: ... (e.g., "Nobody: \nMe: sends 3 follow-up emails and panics")' + 'caption_style' => 'Nobody: ... (e.g., "Nobody: \nMe: sends 3 follow-up emails and panics")', ], [ 'caption_description' => 'A meme caption that starts with "Me trying to", describes a personal struggle or awkward attempt to do something relatable, and ends with no punctuation', - 'caption_style' => 'Me trying to... (e.g., "Me trying to stay calm after 2 hours on hold")' + 'caption_style' => 'Me trying to... (e.g., "Me trying to stay calm after 2 hours on hold")', ], [ 'caption_description' => 'A meme caption that starts with "It\'s giving", followed by a cultural, emotional, or exaggerated vibe that humorously labels the situation, and ends with no punctuation', - 'caption_style' => 'It\'s giving... (e.g., "It\'s giving corporate despair in HD")' + 'caption_style' => 'It\'s giving... (e.g., "It\'s giving corporate despair in HD")', ], [ 'caption_description' => 'A meme caption that starts with "Meanwhile", contrasts expected behavior with a chaotic or unexpected reality, and ends with no punctuation', - 'caption_style' => 'Meanwhile... (e.g., "Meanwhile: I\'m Googling how to quit politely")' + 'caption_style' => 'Meanwhile... (e.g., "Meanwhile: I\'m Googling how to quit politely")', ], [ 'caption_description' => 'A meme caption that starts with "My toxic trait is", followed by an ironic or self-aware confession that highlights flawed logic or behavior, and ends with no punctuation', - 'caption_style' => 'My toxic trait is... (e.g., "My toxic trait is checking Slack and getting mad")' + 'caption_style' => 'My toxic trait is... (e.g., "My toxic trait is checking Slack and getting mad")', ], ]; - - //$random_caption = $captions[rand(0, count($captions) - 1)]; + // $random_caption = $captions[rand(0, count($captions) - 1)]; $random_caption = $captions[count($captions) - 1]; - $response = Http::withHeaders([ 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . env('OPENAI_API_KEY'), + 'Authorization' => 'Bearer '.env('OPENAI_API_KEY'), ]) ->post('https://api.openai.com/v1/responses', [ 'model' => 'gpt-4.1-nano', @@ -231,11 +229,10 @@ public static function getSingleMemeGenerator($user_prompt) if ($response->successful()) { return $data; } else { - throw new \Exception('API request failed: ' . $response->body()); + throw new \Exception('API request failed: '.$response->body()); } } - public static function getOpenAIOutput($data) { // dump($data); diff --git a/app/Helpers/FirstParty/Purchase/PurchaseHelper.php b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php new file mode 100644 index 0000000..53f39d0 --- /dev/null +++ b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php @@ -0,0 +1,169 @@ += 2 && $propertyParts[0] === 'stripe') { + // 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); + } else { + // For non-stripe properties, just prepend 'system.' + $fullPath = 'system.'.$property; + } + + return data_get($plan, $fullPath); + } + + public static function getSubscriptions($type, $enabled = null, $system = false) + { + + $tmp_subscriptions = config('platform.purchases.subscriptions'); + + if (isset($enabled)) { + + $tmp_subscriptions = array_filter($tmp_subscriptions, function ($value) use ($type) { + return $value['type'] == $type; + }); + + $tmp_subscriptions = array_filter($tmp_subscriptions, function ($value) use ($enabled) { + return $value['enabled'] == $enabled; + }); + + if (! $system) { + $tmp_subscriptions = array_map(function ($item) { + + unset($item['system']); + + return $item; + }, $tmp_subscriptions); + } + } + + $env = App::environment(); + + return $tmp_subscriptions; + } + + public static function getOneTimePurchases(?bool $enabled, $system = false) + { + $tmp_otp = config('platform.purchases.one_time'); + + if (isset($enabled)) { + $tmp_otp = array_filter($tmp_otp, function ($value) use ($enabled) { + return $value['enabled'] == $enabled; + }); + } + + if (! $system) { + $tmp_otp = array_map(function ($item) { + + unset($item['system']); + + return $item; + }, $tmp_otp); + } + + return $tmp_otp; + } + + public static function setSystemPlan($arr, $property) + { + foreach ($arr as $key => $item) { + + $arr[$key]['stripe_monthly_price_id'] = PurchaseHelper::getPlanSystemProperty($item, $property); + + // $stripe_monthly_price_id = PurchaseHelper::getPlanSystemProperty($subscription, 'stripe.product_id.month'); + + // $stripe_current_monthly_price_id = PurchaseHelper::getPlanSystemProperty($subscription, 'stripe.current_stripe_price_id.month'); + + // $stripe_all_monthly_price_ids = PurchaseHelper::getPlanSystemProperty($subscription, 'stripe.stripe_price_ids.month'); + + // dump($stripe_monthly_price_id); + // dump($stripe_current_monthly_price_id); + // dump($stripe_all_monthly_price_ids); + } + + return $arr; + } + + public static function filterUnsetSystem($arr) + { + return array_map(function ($item) { + + unset($item['system']); + + return $item; + }, $arr); + } + + public static function filterShowInPricing($arr) + { + return array_filter($arr, function ($item) { + return $item['show_in_pricing'] == true; + }); + } + + private static function getStripeEnvironment() + { + $env = App::environment(); + + // Return 'prod' for production environment, 'test' for everything else + return $env === 'production' ? 'prod' : 'test'; + } + + private static function reiterateArray($arr) + { + $tmp = []; + + foreach ($arr as $key => $item) { + $tmp[] = $item; + } + + return $tmp; + } +} diff --git a/app/Http/Controllers/SocialAuthController.php b/app/Http/Controllers/SocialAuthController.php new file mode 100644 index 0000000..9f79d0e --- /dev/null +++ b/app/Http/Controllers/SocialAuthController.php @@ -0,0 +1,93 @@ +with(['prompt' => 'consent']) + ->redirect(); + } + + public function handleGoogleCallback() + { + try { + if (App::environment('production')) { + $googleUser = Socialite::driver('google')->user(); + } else { + $googleUser = Socialite::driver('google')->user(); + + // /$googleUser = $this->getMockGoogleUser(); + } + + // First, check if the user exists by google_id + $user = User::where('google_id', $googleUser->id)->whereNotNull('google_id')->first(); + + if ($user) { + $this->setupUser($user); + + Auth::login($user); + } else { + // If no user found by google_id, check by email + $user = User::where('email', $googleUser->email)->first(); + + if ($user) { + // If the google_id is empty, update it + if (empty($user->google_id)) { + $user->google_id = $googleUser->id; + $user->save(); + } + $this->setupUser($user); + Auth::login($user); + } else { + // Create a new user if neither exists + $user = User::create([ + 'email' => $googleUser->email, + 'google_id' => $googleUser->id, + ]); + $this->setupUser($user); + + Auth::login($user); + + $intended_route = route('home'); + } + } + + return redirect()->intended(route('home')); + } catch (\Exception $e) { + + throw $e; + $error_message = 'Google login failed. Please try again.'; + if (config('app.debug')) { + $error_message = $e->getMessage(); + } + + return redirect()->route('home')->with('error', $error_message); + } + } + + private function setupUser($user) {} + + private function getMockGoogleUser() + { + // Create a mock user object that mimics Socialite's user structure + return new class + { + public $email = 'memeaigen.com@gmail.com'; + + public $id = 'xxx'; + + // public $email = 'patrick.christener@gmail.com'; + + // public $id = '104771940181889934768'; + + }; + } +} diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php index 7c1889e..a58db30 100644 --- a/app/Http/Controllers/TestController.php +++ b/app/Http/Controllers/TestController.php @@ -6,6 +6,7 @@ use App\Helpers\FirstParty\AI\RunwareAI; use App\Helpers\FirstParty\AspectRatio; use App\Helpers\FirstParty\Meme\MemeGenerator; +use App\Helpers\FirstParty\Purchase\PurchaseHelper; use App\Models\Category; use App\Models\Meme; use App\Models\MemeMedia; @@ -18,6 +19,16 @@ public function index() // } + public function testPurchase() + { + $subscriptions = PurchaseHelper::getPricingPageSubscriptions(); + + $one_time = PurchaseHelper::getPricingPageOneTime(); + + dump($subscriptions); + dump($one_time); + } + public function getSuitableMemeMedia() { $meme = Meme::inRandomOrder()->first(); @@ -68,7 +79,7 @@ public function writeMeme() { $meme_response = OpenAI::getSingleMemeGenerator('Write me 1 meme about adult life'); - //dump($meme_response); + // dump($meme_response); $meme_output = json_decode(OpenAI::getOpenAIOutput($meme_response)); diff --git a/app/Http/Controllers/UserPurchaseController.php b/app/Http/Controllers/UserPurchaseController.php new file mode 100644 index 0000000..72e9397 --- /dev/null +++ b/app/Http/Controllers/UserPurchaseController.php @@ -0,0 +1,132 @@ +json([ + 'success' => [ + 'data' => [ + 'subscription' => $subscriptions[0], + 'one_times' => PurchaseHelper::getPricingPageOneTime(), + ], + ], + ]); + } + + // SUBSCRIBE (RECURRING) + + public function subscribe(Request $request) + { + $price_id = $request->input('price_id'); + + $payload = [ + 'mode' => 'subscription', + 'success_url' => route('subscribe.success').'?'.'session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => route('subscribe.cancelled').'?'.'session_id={CHECKOUT_SESSION_ID}', + 'line_items' => [[ + 'price' => $price_id, + ]], + ]; + + $checkout_session = Auth::user()->checkout([$price_id => 1], $payload); + + Session::put('checkout_session_id', $checkout_session->id); + + return response()->json([ + 'success' => [ + 'data' => [ + 'redirect' => $checkout_session->url, + ], + ], + ]); + } + + public function subscribeSuccess(Request $request) + { + if (! Session::has('checkout_session_id')) { + abort(401); + } + + Session::forget('checkout_session_id'); + + return redirect()->route('home')->with('success', [ + 'message' => 'Thank you for subscribing! Your subscription should be active momentarily. Please refresh the page if you do not see your plan.', + 'action' => 'subscription_success', + ]); + } + + public function subscribeCancelled(Request $request) + { + + if (Session::has('checkout_session_id')) { + Session::forget('checkout_session_id'); + } + + return redirect()->route('home')->with('error', [ + 'message' => "You've decided not to complete the payment at this time. No charges have been made to your account.", + 'action' => 'subscription_cancelled', + ]); + } + + // PURCHASE (ONE TIME) + + public function purchase(Request $request) + { + $price_id = $request->input('price_id'); + + $payload = [ + 'success_url' => route('subscribe.success').'?'.'session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => route('subscribe.cancelled').'?'.'session_id={CHECKOUT_SESSION_ID}', + ]; + + $checkout_session = Auth::user()->checkout([$price_id => 1], $payload); + + Session::put('checkout_session_id', $checkout_session->id); + + return response()->json([ + 'success' => [ + 'data' => [ + 'redirect' => $checkout_session->url, + ], + ], + ]); + } + + public function purchaseSuccess(Request $request) + { + + if (! Session::has('checkout_session_id')) { + abort(401); + } + + Session::forget('checkout_session_id'); + + return redirect()->route('home')->with('success', [ + 'message' => 'Thank you for purchasing! Your purchase should be active momentarily. Please refresh the page if you do not see your plan.', + 'action' => 'purchase_success', + ]); + } + + public function purchaseCancelled(Request $request) + { + if (Session::has('checkout_session_id')) { + Session::forget('checkout_session_id'); + } + + return redirect()->route('home')->with('error', [ + 'message' => "You've decided not to complete the payment at this time. No charges have been made to your account.", + 'action' => 'purchase_cancelled', + ]); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7ba2d61..f2d2562 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -9,9 +9,9 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', - commands: __DIR__.'/../routes/console.php', + web: __DIR__ . '/../routes/web.php', + api: __DIR__ . '/../routes/api.php', + commands: __DIR__ . '/../routes/console.php', health: '/up', then: function () { if (config('platform.general.enable_test_routes')) { @@ -36,7 +36,12 @@ 'cacheResponse' => \Spatie\ResponseCache\Middlewares\CacheResponse::class, 'doNotCacheResponse' => \Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class, ]); + + $middleware->validateCsrfTokens(except: [ + 'stripe/*', + ]); }) + ->withExceptions(function (Exceptions $exceptions) { // })->create(); diff --git a/composer.json b/composer.json index 7eb54d4..0470dad 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,11 @@ "artesaos/seotools": "^1.3", "inertiajs/inertia-laravel": "^2.0", "kalnoy/nestedset": "^6.0", + "laravel/cashier": "^15.7", "laravel/framework": "^12.0", "laravel/horizon": "^5.31", "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.21", "laravel/tinker": "^2.10.1", "league/flysystem-aws-s3-v3": "^3.0", "pbmedia/laravel-ffmpeg": "^8.7", diff --git a/composer.lock b/composer.lock index a79888e..89309ba 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "79e2a426d2f3fc1237920417f93984c5", + "content-hash": "da9cdd29711f1013567f8c238a5f9f24", "packages": [ { "name": "artesaos/seotools", @@ -779,6 +779,69 @@ }, "time": "2023-08-08T05:53:35+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -1593,6 +1656,94 @@ }, "time": "2025-04-22T19:38:02+00:00" }, + { + "name": "laravel/cashier", + "version": "v15.7.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/cashier-stripe.git", + "reference": "e26d51f19d29edc70c01b74e3eca688f487d0987" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/e26d51f19d29edc70c01b74e3eca688f487d0987", + "reference": "e26d51f19d29edc70c01b74e3eca688f487d0987", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/log": "^10.0|^11.0|^12.0", + "illuminate/notifications": "^10.0|^11.0|^12.0", + "illuminate/pagination": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/view": "^10.0|^11.0|^12.0", + "moneyphp/money": "^4.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.1", + "stripe/stripe-php": "^16.2", + "symfony/console": "^6.0|^7.0", + "symfony/http-kernel": "^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.22.1" + }, + "require-dev": { + "dompdf/dompdf": "^2.0|^3.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^8.18|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4|^11.5" + }, + "suggest": { + "dompdf/dompdf": "Required when generating and downloading invoice PDF's using Dompdf (^2.0|^3.0).", + "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Cashier\\CashierServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "15.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Cashier\\": "src/", + "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Dries Vints", + "email": "dries@laravel.com" + } + ], + "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", + "keywords": [ + "billing", + "laravel", + "stripe" + ], + "support": { + "issues": "https://github.com/laravel/cashier/issues", + "source": "https://github.com/laravel/cashier" + }, + "time": "2025-06-10T15:07:05+00:00" + }, { "name": "laravel/framework", "version": "v12.9.2", @@ -2072,6 +2223,78 @@ }, "time": "2025-03-19T13:51:03+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.21.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "d83639499ad14985c9a6a9713b70073300ce998d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/d83639499ad14985c9a6a9713b70073300ce998d", + "reference": "d83639499ad14985c9a6a9713b70073300ce998d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2025-05-19T12:56:37+00:00" + }, { "name": "laravel/tinker", "version": "v2.10.1", @@ -2570,6 +2793,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, { "name": "league/uri", "version": "7.5.1", @@ -2744,6 +3043,96 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "moneyphp/money", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/money.git", + "reference": "1a23f0e1b22e2c59ed5ed70cfbe4cbe696be9348" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/money/zipball/1a23f0e1b22e2c59ed5ed70cfbe4cbe696be9348", + "reference": "1a23f0e1b22e2c59ed5ed70cfbe4cbe696be9348", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-filter": "*", + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^12.0", + "doctrine/instantiator": "^1.5.0 || ^2.0", + "ext-gmp": "*", + "ext-intl": "*", + "florianv/exchanger": "^2.8.1", + "florianv/swap": "^4.3.0", + "moneyphp/crypto-currencies": "^1.1.0", + "moneyphp/iso-currencies": "^3.4", + "php-http/message": "^1.16.0", + "php-http/mock-client": "^1.6.0", + "phpbench/phpbench": "^1.2.5", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.9", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.9", + "psr/cache": "^1.0.1 || ^2.0 || ^3.0", + "ticketswap/phpstan-error-formatter": "^1.1" + }, + "suggest": { + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", + "keywords": [ + "Value Object", + "money", + "vo" + ], + "support": { + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v4.7.1" + }, + "time": "2025-06-06T07:12:38+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -3312,6 +3701,123 @@ ], "time": "2024-11-21T10:39:51+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "pbmedia/laravel-ffmpeg", "version": "8.7.1", @@ -3612,6 +4118,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.46", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2025-06-26T16:29:55+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4796,6 +5412,65 @@ ], "time": "2025-01-13T13:04:43+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v16.6.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "d6de0a536f00b5c5c74f36b8f4d0d93b035499ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/d6de0a536f00b5c5c74f36b8f4d0d93b035499ff", + "reference": "d6de0a536f00b5c5c74f36b8f4d0d93b035499ff", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v16.6.0" + }, + "time": "2025-02-24T22:35:29+00:00" + }, { "name": "symfony/cache", "version": "v7.3.0", @@ -6077,6 +6752,90 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/763d2a91fea5681509ca01acbc1c5e450d127811", + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-21T18:38:29+00:00" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.31.0", diff --git a/config/cashier.php b/config/cashier.php new file mode 100644 index 0000000..4a9b024 --- /dev/null +++ b/config/cashier.php @@ -0,0 +1,127 @@ + env('STRIPE_KEY'), + + 'secret' => env('STRIPE_SECRET'), + + /* + |-------------------------------------------------------------------------- + | Cashier Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Cashier's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + + 'path' => env('CASHIER_PATH', 'stripe'), + + /* + |-------------------------------------------------------------------------- + | Stripe Webhooks + |-------------------------------------------------------------------------- + | + | Your Stripe webhook secret is used to prevent unauthorized requests to + | your Stripe webhook handling controllers. The tolerance setting will + | check the drift between the current time and the signed request's. + | + */ + + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + 'events' => WebhookCommand::DEFAULT_EVENTS, + ], + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Stripe. + | + */ + + 'currency' => env('CASHIER_CURRENCY', 'usd'), + + /* + |-------------------------------------------------------------------------- + | Currency Locale + |-------------------------------------------------------------------------- + | + | This is the default locale in which your money values are formatted in + | for display. To utilize other locales besides the default en locale + | verify you have the "intl" PHP extension installed on the system. + | + */ + + 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Payment Confirmation Notification + |-------------------------------------------------------------------------- + | + | If this setting is enabled, Cashier will automatically notify customers + | whose payments require additional verification. You should listen to + | Stripe's webhooks in order for this feature to function correctly. + | + */ + + 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), + + /* + |-------------------------------------------------------------------------- + | Invoice Settings + |-------------------------------------------------------------------------- + | + | The following options determine how Cashier invoices are converted from + | HTML into PDFs. You're free to change the options based on the needs + | of your application or your preferences regarding invoice styling. + | + */ + + 'invoices' => [ + 'renderer' => env('CASHIER_INVOICE_RENDERER', DompdfInvoiceRenderer::class), + + 'options' => [ + // Supported: 'letter', 'legal', 'A4' + 'paper' => env('CASHIER_PAPER', 'letter'), + + 'remote_enabled' => env('CASHIER_REMOTE_ENABLED', false), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Stripe Logger + |-------------------------------------------------------------------------- + | + | This setting defines which logging channel will be used by the Stripe + | library to write log messages. You are free to specify any of your + | logging channels listed inside the "logging" configuration file. + | + */ + + 'logger' => env('CASHIER_LOGGER'), + +]; diff --git a/config/platform/general.php b/config/platform/general.php index 93f27a6..965a517 100644 --- a/config/platform/general.php +++ b/config/platform/general.php @@ -2,4 +2,6 @@ return [ 'enable_test_routes' => env('ENABLE_TEST_ROUTES', false), + + 'authed_route_redirect' => 'home', ]; diff --git a/config/platform/purchases/one_time.php b/config/platform/purchases/one_time.php new file mode 100644 index 0000000..a7cc347 --- /dev/null +++ b/config/platform/purchases/one_time.php @@ -0,0 +1,37 @@ + 'ala-500-credits', + 'show_in_pricing' => true, + 'type' => 'alacarte_credits', + 'enabled' => true, + 'name' => '500 Credit Packs', + 'description' => 'Use credits to automatically generate captions & backgrounds for your memes.', + 'amount' => 4, + 'currency' => 'usd', + 'symbol' => '$', + 'system' => [ + 'credits' => 500, + 'stripe' => [ + 'product_id' => [ + 'test' => 'prod_SaY9YLBtUR5Ucb', + 'prod' => 'prod_XXXXXXXXXXXXX', + ], + 'current_stripe_price_id' => [ + 'test' => 'price_1RfN37EEXQJo9EEOb6UCQVEx', + 'prod' => 'price_XXXXXXXXXXXXX', + ], + 'stripe_price_ids' => [ + 'test' => [ + 'price_1RfN37EEXQJo9EEOb6UCQVEx', + ], + 'prod' => [ + 'price_XXXXXXXXXXXX', + ], + ], + ], + + ], + ], +]; diff --git a/config/platform/purchases/subscriptions.php b/config/platform/purchases/subscriptions.php new file mode 100644 index 0000000..0889ae4 --- /dev/null +++ b/config/platform/purchases/subscriptions.php @@ -0,0 +1,134 @@ + 'free', + 'show_in_pricing' => false, + 'type' => 'subscription_plans', + 'enabled' => true, + 'name' => 'Free', + 'amount' => 0, + 'currency' => 'usd', + 'symbol' => '$', + 'primary_interval' => 'month', + 'features' => [ + [ + 'text' => 'Unlimited watermarked videos', + 'available' => true, + ], + [ + 'text' => 'Memes & Background Libraries', + 'available' => true, + ], + ], + 'system' => [ + 'non_watermark_videos' => 0, + ], + ], + [ + 'id' => 'personal-creator', + 'show_in_pricing' => true, + 'type' => 'subscription_plans', + 'enabled' => true, + 'name' => 'Personal Creator', + 'amount' => 4, + 'currency' => 'usd', + 'symbol' => '$', + 'primary_interval' => 'month', + 'features' => [ + [ + 'text' => '50 non-watermarked videos', + 'available' => true, + ], + ], + 'system' => [ + 'non_watermark_videos' => 50, + 'stripe' => [ + 'product_id' => [ + 'test' => [ + 'month' => 'prod_SaY8TGjiPi5hWu', + ], + 'prod' => [ + 'month' => 'prod_XXXXXXXXXXXXX', + ], + ], + 'current_stripe_price_id' => [ + 'test' => [ + 'month' => 'price_1RfN2VEEXQJo9EEOzjPI2HGt', + ], + 'prod' => [ + 'month' => 'price_XXXXXXXXXXXXX', + ], + ], + 'stripe_price_ids' => [ + 'test' => [ + 'month' => [ + 'price_1RfN2VEEXQJo9EEOzjPI2HGt', + ], + ], + 'prod' => [ + 'month' => [ + 'price_XXXXXXXXXXXX', + ], + ], + ], + ], + + ], + ], + [ + 'id' => 'sub-500-credits', + 'show_in_pricing' => false, + 'type' => 'subscription_credits', + 'enabled' => true, + 'name' => '500 Credits / month', + 'amount' => 3, + 'currency' => 'usd', + 'symbol' => '$', + 'primary_interval' => 'month', + 'features' => [ + [ + 'text' => '500 Credits', + 'available' => true, + ], + ], + 'system' => [ + 'credits' => 500, + 'stripe' => [ + 'product_id' => [ + 'test' => [ + 'month' => 'prod_XXXXXXXXXXXXX', + ], + 'prod' => [ + 'month' => 'prod_XXXXXXXXXXXXX', + ], + ], + 'current_stripe_price_id' => [ + 'test' => [ + 'month' => 'price_XXXXXXXXXXXXX', + ], + 'prod' => [ + 'month' => 'price_XXXXXXXXXXXXX', + ], + ], + 'stripe_price_ids' => [ + 'test' => [ + 'month' => [ + 'price_XXXXXXXXXXXXX', + 'price_XXXXXXXXXXXXX', + ], + ], + 'prod' => [ + 'month' => [ + 'price_XXXXXXXXXXXX', + 'price_XXXXXXXXXXXXX', + ], + ], + ], + ], + + ], + ], +]; diff --git a/config/services.php b/config/services.php index c464da0..5848962 100644 --- a/config/services.php +++ b/config/services.php @@ -47,4 +47,10 @@ 'api_key' => env('OPENAI_API_KEY'), ], + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_CLIENT_REDIRECT_URI'), + ], + ]; diff --git a/database/migrations/2025_07_01_074420_add_google_id_to_users_table.php b/database/migrations/2025_07_01_074420_add_google_id_to_users_table.php new file mode 100644 index 0000000..a18e65a --- /dev/null +++ b/database/migrations/2025_07_01_074420_add_google_id_to_users_table.php @@ -0,0 +1,28 @@ +string('google_id')->nullable()->unique(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('google_id'); + }); + } +}; diff --git a/database/migrations/2025_07_01_112905_set_password_null_to_users_table.php b/database/migrations/2025_07_01_112905_set_password_null_to_users_table.php new file mode 100644 index 0000000..3bd91b0 --- /dev/null +++ b/database/migrations/2025_07_01_112905_set_password_null_to_users_table.php @@ -0,0 +1,28 @@ +string('password')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('password')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2025_07_01_114203_create_customer_columns.php b/database/migrations/2025_07_01_114203_create_customer_columns.php new file mode 100644 index 0000000..974b381 --- /dev/null +++ b/database/migrations/2025_07_01_114203_create_customer_columns.php @@ -0,0 +1,40 @@ +string('stripe_id')->nullable()->index(); + $table->string('pm_type')->nullable(); + $table->string('pm_last_four', 4)->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex([ + 'stripe_id', + ]); + + $table->dropColumn([ + 'stripe_id', + 'pm_type', + 'pm_last_four', + 'trial_ends_at', + ]); + }); + } +}; diff --git a/database/migrations/2025_07_01_114204_create_subscriptions_table.php b/database/migrations/2025_07_01_114204_create_subscriptions_table.php new file mode 100644 index 0000000..ccbcc6d --- /dev/null +++ b/database/migrations/2025_07_01_114204_create_subscriptions_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id'); + $table->string('type'); + $table->string('stripe_id')->unique(); + $table->string('stripe_status'); + $table->string('stripe_price')->nullable(); + $table->integer('quantity')->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + + $table->index(['user_id', 'stripe_status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/database/migrations/2025_07_01_114205_create_subscription_items_table.php b/database/migrations/2025_07_01_114205_create_subscription_items_table.php new file mode 100644 index 0000000..420e23f --- /dev/null +++ b/database/migrations/2025_07_01_114205_create_subscription_items_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('subscription_id'); + $table->string('stripe_id')->unique(); + $table->string('stripe_product'); + $table->string('stripe_price'); + $table->integer('quantity')->nullable(); + $table->timestamps(); + + $table->index(['subscription_id', 'stripe_price']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscription_items'); + } +}; diff --git a/package-lock.json b/package-lock.json index 2e4dd2f..00e8237 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "cmdk": "^1.1.1", "concurrently": "^9.0.1", "date-fns": "^4.1.0", + "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "input-otp": "^1.4.2", @@ -55,6 +56,7 @@ "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "mitt": "^3.0.1", + "motion": "^12.19.2", "next-themes": "^0.4.6", "react": "^19.0.0", "react-day-picker": "^9.7.0", @@ -4617,6 +4619,15 @@ "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "license": "MIT" }, + "node_modules/embla-carousel-autoplay": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz", + "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/embla-carousel-react": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", @@ -5312,6 +5323,33 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "12.19.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.2.tgz", + "integrity": "sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.19.0", + "motion-utils": "^12.19.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6686,6 +6724,47 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion": { + "version": "12.19.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.19.2.tgz", + "integrity": "sha512-Yb69HXE4ryhVd1xwpgWMMQAQgqEGMSGWG+NOumans2fvSCtT8gsj8JK7jhcGnc410CLT3BFPgquP67zmjbA5Jw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.19.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz", + "integrity": "sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.19.0" + } + }, + "node_modules/motion-utils": { + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", + "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index a639947..babf81a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "cmdk": "^1.1.1", "concurrently": "^9.0.1", "date-fns": "^4.1.0", + "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "input-otp": "^1.4.2", @@ -73,6 +74,7 @@ "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "mitt": "^3.0.1", + "motion": "^12.19.2", "next-themes": "^0.4.6", "react": "^19.0.0", "react-day-picker": "^9.7.0", diff --git a/resources/css/app.css b/resources/css/app.css index 61d2ea8..db0f676 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -172,6 +172,23 @@ @theme inline { --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --animate-pulse: pulse var(--duration) ease-out infinite; + @keyframes pulse { + 0%, 100% { + boxShadow: 0 0 0 0 var(--pulse-color); + } + 50% { + boxShadow: 0 0 0 8px var(--pulse-color); + } + } + @keyframes pulse { + 0%, 100% { + boxShadow: 0 0 0 0 var(--pulse-color); + } + 50% { + boxShadow: 0 0 0 8px var(--pulse-color); + } + } } /* @@ -185,4 +202,4 @@ @layer base { body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 9a8aaf6..17de309 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -5,7 +5,9 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createRoot } from 'react-dom/client'; import { ErrorBoundary } from 'react-error-boundary'; import DetailedErrorFallback from './components/custom/detailed-error-fallback'; // Import your component +import { Toaster } from './components/ui/sonner'; import { initializeTheme } from './hooks/use-appearance'; +import AuthDialog from './modules/auth/AuthDialog'; import { AxiosProvider } from './plugins/AxiosContext'; import { MittProvider } from './plugins/MittContext'; @@ -34,6 +36,8 @@ createInertiaApp({ > + + diff --git a/resources/js/components/magicui/pulsating-button.tsx b/resources/js/components/magicui/pulsating-button.tsx new file mode 100644 index 0000000..78d36ad --- /dev/null +++ b/resources/js/components/magicui/pulsating-button.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface PulsatingButtonProps + extends React.ButtonHTMLAttributes { + pulseColor?: string; + duration?: string; +} + +export const PulsatingButton = React.forwardRef< + HTMLButtonElement, + PulsatingButtonProps +>( + ( + { + className, + children, + pulseColor = "#808080", + duration = "1.5s", + ...props + }, + ref, + ) => { + return ( + + ); + }, +); + +PulsatingButton.displayName = "PulsatingButton"; diff --git a/resources/js/components/magicui/sparkles-text.tsx b/resources/js/components/magicui/sparkles-text.tsx new file mode 100644 index 0000000..17b3a36 --- /dev/null +++ b/resources/js/components/magicui/sparkles-text.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { motion } from "motion/react"; +import { CSSProperties, ReactElement, useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface Sparkle { + id: string; + x: string; + y: string; + color: string; + delay: number; + scale: number; + lifespan: number; +} + +const Sparkle: React.FC = ({ id, x, y, color, delay, scale }) => { + return ( + + + + ); +}; + +interface SparklesTextProps { + /** + * @default
+ * @type ReactElement + * @description + * The component to be rendered as the text + * */ + as?: ReactElement; + + /** + * @default "" + * @type string + * @description + * The className of the text + */ + className?: string; + + /** + * @required + * @type ReactNode + * @description + * The content to be displayed + * */ + children: React.ReactNode; + + /** + * @default 10 + * @type number + * @description + * The count of sparkles + * */ + sparklesCount?: number; + + /** + * @default "{first: '#9E7AFF', second: '#FE8BBB'}" + * @type string + * @description + * The colors of the sparkles + * */ + colors?: { + first: string; + second: string; + }; +} + +export const SparklesText: React.FC = ({ + children, + colors = { first: "#9E7AFF", second: "#FE8BBB" }, + className, + sparklesCount = 10, + ...props +}) => { + const [sparkles, setSparkles] = useState([]); + + useEffect(() => { + const generateStar = (): Sparkle => { + const starX = `${Math.random() * 100}%`; + const starY = `${Math.random() * 100}%`; + const color = Math.random() > 0.5 ? colors.first : colors.second; + const delay = Math.random() * 2; + const scale = Math.random() * 1 + 0.3; + const lifespan = Math.random() * 10 + 5; + const id = `${starX}-${starY}-${Date.now()}`; + return { id, x: starX, y: starY, color, delay, scale, lifespan }; + }; + + const initializeStars = () => { + const newSparkles = Array.from({ length: sparklesCount }, generateStar); + setSparkles(newSparkles); + }; + + const updateStars = () => { + setSparkles((currentSparkles) => + currentSparkles.map((star) => { + if (star.lifespan <= 0) { + return generateStar(); + } else { + return { ...star, lifespan: star.lifespan - 0.1 }; + } + }), + ); + }; + + initializeStars(); + const interval = setInterval(updateStars, 100); + + return () => clearInterval(interval); + }, [colors.first, colors.second, sparklesCount]); + + return ( +
+ + {sparkles.map((sparkle) => ( + + ))} + {children} + +
+ ); +}; diff --git a/resources/js/modules/auth/AuthDialog.jsx b/resources/js/modules/auth/AuthDialog.jsx new file mode 100644 index 0000000..9cfb80a --- /dev/null +++ b/resources/js/modules/auth/AuthDialog.jsx @@ -0,0 +1,78 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { useMitt } from '@/plugins/MittContext'; +import { useEffect, useState } from 'react'; + +const AuthDialog = ({ onOpenChange }) => { + const emitter = useMitt(); + + const [isLogin, setIsLogin] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + const handleGoogleLogin = () => { + window.location.href = route('auth.google.redirect'); + }; + + // Listen for text element selection (but don't auto-open sidebar) + useEffect(() => { + const handleOpenAuth = () => { + setIsOpen(true); + }; + + emitter.on('401', handleOpenAuth); + + return () => { + emitter.off('401', handleOpenAuth); + }; + }, [emitter]); + + return ( + + + + {isLogin ? 'Welcome back' : 'Create account'} + + {isLogin ? 'Sign in to your account to continue' : 'Sign up to get started'} + + + +
+ +
+ +
By continuing, you agree to our Terms of Service and Privacy Policy
+ +
+ {isLogin ? "Don't have an account?" : 'Already have an account?'}{' '} + +
+
+
+ ); +}; + +export default AuthDialog; diff --git a/resources/js/modules/editor/editor.jsx b/resources/js/modules/editor/editor.jsx index 47b3904..407592d 100644 --- a/resources/js/modules/editor/editor.jsx +++ b/resources/js/modules/editor/editor.jsx @@ -8,6 +8,7 @@ import useVideoEditorStore from '@/stores/VideoEditorStore'; // Import fonts first - this loads all Fontsource packages import '@/modules/editor/fonts'; +import UpgradeSheet from '../upgrade/upgrade-sheet'; import EditNavSidebar from './partials/edit-nav-sidebar'; import EditSidebar from './partials/edit-sidebar'; import EditorCanvas from './partials/editor-canvas'; @@ -216,6 +217,7 @@ const Editor = () => { )}
+ ); }; diff --git a/resources/js/modules/editor/partials/edit-nav-sidebar.jsx b/resources/js/modules/editor/partials/edit-nav-sidebar.jsx index 4dda09d..c74459d 100644 --- a/resources/js/modules/editor/partials/edit-nav-sidebar.jsx +++ b/resources/js/modules/editor/partials/edit-nav-sidebar.jsx @@ -1,11 +1,12 @@ +import { usePage } from '@inertiajs/react'; + import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import useLocalSettingsStore from '@/stores/localSettingsStore'; -import { SettingsIcon } from 'lucide-react'; export default function EditNavSidebar({ isOpen, onClose }) { + const { auth } = usePage().props; + const { getSetting, setSetting } = useLocalSettingsStore(); return ( @@ -17,36 +18,22 @@ export default function EditNavSidebar({ isOpen, onClose }) { -
- - - - - - - Settings - Change your settings here. - +
+
+ {/* {!auth.user && } + {!auth.user && } */} +
-
- setSetting('genAlphaSlang', !getSetting('genAlphaSlang'))} - /> - -
- - - -
+
+ +
diff --git a/resources/js/modules/editor/partials/editor-header.jsx b/resources/js/modules/editor/partials/editor-header.jsx index 135273e..c2bf09f 100644 --- a/resources/js/modules/editor/partials/editor-header.jsx +++ b/resources/js/modules/editor/partials/editor-header.jsx @@ -1,15 +1,18 @@ import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; -import CoinIcon from '@/reusables/coin-icon'; +import { useMitt } from '@/plugins/MittContext'; +import CartIcon from '@/reusables/cart-icon'; import useLocalSettingsStore from '@/stores/localSettingsStore'; import { Menu } from 'lucide-react'; -import { useState } from 'react'; const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = false }) => { const { getSetting } = useLocalSettingsStore(); - const [openCoinDialog, setOpenCoinDialog] = useState(false); + const emitter = useMitt(); + + const openUpgradeSheet = () => { + emitter.emit('openUpgradeSheet'); + }; return (
@@ -19,33 +22,16 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal

MEMEAIGEN

- - - setOpenCoinDialog(open)}> - - - {getSetting('genAlphaSlang') ? 'Bruh' : 'Feature coming soon'} - - {getSetting('genAlphaSlang') - ? "No cap, soon you'll be able to get AI cooking memes that absolutely slay! But lowkey fam, we gotta focus on making these core features bussin' first." - : "Soon you'll be able to prompt AI to generate memes! Let us focus on nailing the core features first."} - - - -
- {/* - Note: You can turn {getSetting('genAlphaSlang') ? 'off' : 'on'} gen alpha slang in Settings. - */} - -
-
-
-
); }; diff --git a/resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx b/resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx new file mode 100644 index 0000000..5663106 --- /dev/null +++ b/resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from '@/components/ui/carousel'; +import { cn } from '@/lib/utils'; +import Autoplay from 'embla-carousel-autoplay'; +import { CheckCircle, Handshake, Lock, Zap } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +const upgradePlanData = [ + { + icon: Zap, + title: 'Remove watermarks', + description: 'Export up to 50 watermark-free videos, perfect for posting to your creator channel (8¢ per video)', + }, + { + icon: CheckCircle, + title: 'Personal license included', + description: 'Full rights to use videos for personal social media, creator and non-commercial projects', + }, + { + icon: Lock, + title: 'Lock in your pricing', + description: 'Subscribe now and keep this price forever - even when we raise prices for new users', + }, + { + icon: Handshake, + title: 'Support our development', + description: 'Help us to improve and grow so you get the best features & experience', + }, +]; + +const UpgradePlanCarousel = () => { + const [api, setApi] = useState(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); + + useEffect(() => { + if (!api) { + return; + } + + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + + api.on('select', () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + + const scrollTo = (index: number) => { + api?.scrollTo(index); + }; + + return ( +
+ + + {upgradePlanData.map((item, index) => { + const IconComponent = item.icon; + return ( + +
+
+
+ +
+

{item.title}

+

{item.description}

+
+
+
+ ); + })} +
+
+ + {/* Centered Dot Navigation */} +
+ {Array.from({ length: count }, (_, index) => ( +
+
+ ); +}; + +export default UpgradePlanCarousel; diff --git a/resources/js/modules/upgrade/upgrade-sheet.jsx b/resources/js/modules/upgrade/upgrade-sheet.jsx new file mode 100644 index 0000000..a2e4774 --- /dev/null +++ b/resources/js/modules/upgrade/upgrade-sheet.jsx @@ -0,0 +1,198 @@ +// 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 { useEffect, useState } from 'react'; +import UpgradePlanCarousel from './partials/upgrade-plan-carousel.tsx'; + +const UpgradeSheet = () => { + const { subscription, one_times, isFetchingPricing, fetchPricing, isCheckingOut, checkoutSubscribe } = usePricingStore(); + + // 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.

This plan also includes a Personal license, which allows you to use the app for personal social media, creator and non-commercial projects.

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.

We believe that creators deserve access to high quality videos, and we're committed to making it accessible to every creator.

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.

The Personal Creator Plan is designed for personal social media, creator and non-commercial projects.

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.

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.

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.

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); + }; + + return ( + + + + + Store + + + + +
+ {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
+
+
+ ) : ( + isFetchingPricing && ( +
+ +
+ ) + )} + +
+
Buy Credit Packs
+
+ Unlock AI meme captions and backgrounds with credits. Perfect for overcoming creator's block and discovering new concepts. +
+ +
+
+
+
+
+ + 500 Credit Pack +
+
+ Approx. 250 AI captions & 250 AI backgrounds +
+
+ +
+
$4.00
+
+
+ +
+
+
+ +
+
Frequently Asked Questions
+ + {faqData.map((faq, index) => ( + + {faq.q} + +
+ + + ))} + +
+
+ + + + + + + + + ); +}; + +export default UpgradeSheet; diff --git a/resources/js/plugins/axios-plugin.jsx b/resources/js/plugins/axios-plugin.jsx index f7b334a..97d1afc 100644 --- a/resources/js/plugins/axios-plugin.jsx +++ b/resources/js/plugins/axios-plugin.jsx @@ -1,5 +1,6 @@ import axios from 'axios'; import { toast } from 'sonner'; +import { emitter } from './MittContext'; const axiosInstance = axios.create({ withCredentials: true, @@ -16,6 +17,13 @@ axiosInstance.interceptors.response.use( toast.error(error.response.data.message + ' Please try again later.'); } } + + if (error.response && error.response.status === 401) { + //toast.error('You are not logged in. Please login to continue.'); + + emitter.emit('401'); + } + return Promise.reject(error); }, ); diff --git a/resources/js/reusables/cart-icon.jsx b/resources/js/reusables/cart-icon.jsx new file mode 100644 index 0000000..5954249 --- /dev/null +++ b/resources/js/reusables/cart-icon.jsx @@ -0,0 +1,48 @@ +const CartIcon = ({ className }) => { + return ( + + + + + + + + + + + + + ); +}; + +export default CartIcon; diff --git a/resources/js/reusables/coin-icon.tsx b/resources/js/reusables/coin-icon.jsx similarity index 95% rename from resources/js/reusables/coin-icon.tsx rename to resources/js/reusables/coin-icon.jsx index d4652b8..fe4f01c 100644 --- a/resources/js/reusables/coin-icon.tsx +++ b/resources/js/reusables/coin-icon.jsx @@ -1,10 +1,4 @@ -import React from 'react'; - -interface CoinIconProps { - className?: string; -} - -const CoinIcon: React.FC = ({ className }) => { +const CoinIcon = ({ className }) => { return ( diff --git a/resources/js/stores/PricingStore.js b/resources/js/stores/PricingStore.js new file mode 100644 index 0000000..109817f --- /dev/null +++ b/resources/js/stores/PricingStore.js @@ -0,0 +1,95 @@ +import axiosInstance from '@/plugins/axios-plugin'; +import { mountStoreDevtool } from 'simple-zustand-devtools'; +import { toast } from 'sonner'; +import { route } from 'ziggy-js'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +const usePricingStore = create( + devtools((set, get) => ({ + // Pricing Plans + subscription: null, + one_times: [], + isFetchingPricing: false, + + isCheckingOut: false, + + checkoutSubscribe: async (price_id) => { + console.log('checkoutSubscribe', price_id); + + set({ isCheckingOut: true }); + try { + const response = await axiosInstance.post(route('api.user.subscribe'), { price_id: price_id }); + + if (response?.data?.success?.data) { + if (response.data.success.data.redirect) { + window.location.href = response.data.success.data.redirect; + } + } else { + throw 'Invalid API response'; + } + } catch (error) { + console.error(route('api.user.subscribe')); + console.error('Error fetching:', error); + set({ isCheckingOut: false }); + if (error?.response?.data?.error?.message?.length > 0) { + toast.error(error.response.data.error.message); + } + throw error; + } finally { + set({ isCheckingOut: false }); + } + }, + + // Fetch backgrounds + fetchPricing: async () => { + set({ isFetchingPricing: true }); + try { + const response = await axiosInstance.post(route('api.pricing_page')); + + if (response?.data?.success?.data) { + set({ + subscription: response.data.success.data.subscription, + one_times: response.data.success.data.one_times, + }); + return response.data.success.data; + } else { + throw 'Invalid API response'; + } + } catch (error) { + console.error(route('api.pricing_page')); + console.error('Error fetching:', error); + set({ isFetchingPricing: false }); + if (error?.response?.data?.error?.message?.length > 0) { + toast.error(error.response.data.error.message); + } + throw error; + } finally { + set({ isFetchingPricing: false }); + } + }, + + // Reset store to default state + restoreMemeStateToDefault: () => { + console.log('restoreMemeStateToDefault'); + set({ + memes: [], + backgrounds: [], + isFetchingMemes: false, + isFetchingBackgrounds: false, + selectedMeme: null, + selectedBackground: null, + }); + }, + })), + { + name: 'MemeStore', + store: 'MemeStore', + }, +); + +if (import.meta.env.APP_ENV === 'local') { + mountStoreDevtool('PricingStore', usePricingStore); +} + +export default usePricingStore; diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js index 94bc336..a2f9b69 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":{"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.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"]},"dashboard":{"uri":"dashboard","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.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 d23e1ab..1209ea6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Auth\SanctumAuthController; use App\Http\Controllers\FrontMediaController; +use App\Http\Controllers\UserPurchaseController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -10,6 +11,8 @@ Route::post('/login', [SanctumAuthController::class, 'login']); }); +Route::post('/pricing', [UserPurchaseController::class, 'pricingPage'])->name('api.pricing_page'); + Route::middleware('auth:sanctum')->group(function () { Route::group(['prefix' => 'user'], function () { @@ -17,6 +20,10 @@ return $request->user(); }); + Route::post('/subscribe', [UserPurchaseController::class, 'subscribe'])->name('api.user.subscribe'); + + Route::post('/purchase', [UserPurchaseController::class, 'purchase'])->name('api.user.purchase'); + Route::post('/logout', [SanctumAuthController::class, 'logout']); }); }); diff --git a/routes/test.php b/routes/test.php index fdc6d0f..0a6a1b0 100644 --- a/routes/test.php +++ b/routes/test.php @@ -4,6 +4,8 @@ Route::get('/', [TestController::class, 'index']); +Route::get('/testPurchase', [TestController::class, 'testPurchase']); + Route::get('/populateDuration', [TestController::class, 'populateDuration']); Route::get('/writeMeme', [TestController::class, 'writeMeme']); diff --git a/routes/web.php b/routes/web.php index a0919e2..ebd9575 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,16 +3,42 @@ use App\Http\Controllers\AdminBackgroundGenerationController; use App\Http\Controllers\AdminDashboardController; use App\Http\Controllers\FrontHomeController; +use App\Http\Controllers\SocialAuthController; use App\Http\Controllers\UserDashboardController; +use App\Http\Controllers\UserPurchaseController; use App\Http\Middleware\AdminMiddleware; use Illuminate\Support\Facades\Route; if (App::environment('local')) { + Route::prefix('auth')->group(function () { + + Route::prefix('google')->group(function () { + + Route::get('redirect', [SocialAuthController::class, 'redirectToGoogle'])->name('auth.google.redirect'); + + Route::get('callback', [SocialAuthController::class, 'handleGoogleCallback'])->name('auth.google.callback'); + }); + }); + Route::middleware(['auth', 'verified'])->group(function () { Route::get('dashboard', [UserDashboardController::class, 'index'])->name('dashboard'); + Route::prefix('subscribe')->group(function () { + + Route::get('success', [UserPurchaseController::class, 'subscribeSuccess'])->name('subscribe.success'); + + Route::get('cancelled', [UserPurchaseController::class, 'subscribeCancelled'])->name('subscribe.cancelled'); + }); + + Route::prefix('purchase')->group(function () { + + Route::get('success', [UserPurchaseController::class, 'purchaseSuccess'])->name('purchase.success'); + + Route::get('cancelled', [UserPurchaseController::class, 'purchaseCancelled'])->name('purchase.cancelled'); + }); + Route::prefix('admin')->middleware([AdminMiddleware::class])->group(function () { Route::get('/', [AdminDashboardController::class, 'index'])->name('admin.dashboard');