Update
This commit is contained in:
11
MEMEAIGEN/PRICING.bru
Normal file
11
MEMEAIGEN/PRICING.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: PRICING
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://memeaigen.test/api/pricing
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
9
MEMEAIGEN/bruno.json
Normal file
9
MEMEAIGEN/bruno.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "MEMEAIGEN",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
3
STRIPE.md
Normal file
3
STRIPE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
stripe login
|
||||
|
||||
stripe listen --forward-to https://memeaigen.test/stripe/webhook
|
||||
149
_ide_helper.php
149
_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 {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
169
app/Helpers/FirstParty/Purchase/PurchaseHelper.php
Normal file
169
app/Helpers/FirstParty/Purchase/PurchaseHelper.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\Purchase;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class PurchaseHelper
|
||||
{
|
||||
public static function getPricingPageSubscriptions()
|
||||
{
|
||||
|
||||
$subscriptions = self::getSubscriptions('subscription_plans', true, true);
|
||||
|
||||
$subscriptions = self::filterShowInPricing($subscriptions);
|
||||
|
||||
$subscriptions = self::setSystemPlan($subscriptions, 'stripe.current_stripe_price_id.month');
|
||||
|
||||
$subscriptions = self::filterUnsetSystem($subscriptions);
|
||||
|
||||
$subscriptions = self::reiterateArray($subscriptions);
|
||||
|
||||
return $subscriptions;
|
||||
}
|
||||
|
||||
public static function getPricingPageOneTime()
|
||||
{
|
||||
$one_time = self::getOneTimePurchases(true, true);
|
||||
|
||||
$one_time = PurchaseHelper::filterShowInPricing($one_time);
|
||||
|
||||
$one_time = PurchaseHelper::setSystemPlan($one_time, 'stripe.current_stripe_price_id');
|
||||
|
||||
$one_time = PurchaseHelper::filterUnsetSystem($one_time);
|
||||
|
||||
$one_time = self::reiterateArray($one_time);
|
||||
|
||||
return $one_time;
|
||||
}
|
||||
|
||||
public static function getPlanSystemProperty($plan, $property)
|
||||
{
|
||||
// Get the environment (test or prod)
|
||||
$environment = self::getStripeEnvironment();
|
||||
|
||||
// Split the property path
|
||||
$propertyParts = explode('.', $property);
|
||||
|
||||
// Check if this is a stripe-related property that needs environment injection
|
||||
if (count($propertyParts) >= 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;
|
||||
}
|
||||
}
|
||||
93
app/Http/Controllers/SocialAuthController.php
Normal file
93
app/Http/Controllers/SocialAuthController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class SocialAuthController extends Controller
|
||||
{
|
||||
public function redirectToGoogle()
|
||||
{
|
||||
return Socialite::driver('google')
|
||||
->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';
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
132
app/Http/Controllers/UserPurchaseController.php
Normal file
132
app/Http/Controllers/UserPurchaseController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\FirstParty\Purchase\PurchaseHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class UserPurchaseController extends Controller
|
||||
{
|
||||
public function pricingPage(Request $request)
|
||||
{
|
||||
|
||||
$subscriptions = PurchaseHelper::getPricingPageSubscriptions();
|
||||
|
||||
return response()->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
761
composer.lock
generated
761
composer.lock
generated
@@ -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",
|
||||
|
||||
127
config/cashier.php
Normal file
127
config/cashier.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Cashier\Console\WebhookCommand;
|
||||
use Laravel\Cashier\Invoices\DompdfInvoiceRenderer;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stripe Keys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Stripe publishable key and secret key give you access to Stripe's
|
||||
| API. The "publishable" key is typically used when interacting with
|
||||
| Stripe.js while the "secret" key accesses private API endpoints.
|
||||
|
|
||||
*/
|
||||
|
||||
'key' => 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'),
|
||||
|
||||
];
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
return [
|
||||
'enable_test_routes' => env('ENABLE_TEST_ROUTES', false),
|
||||
|
||||
'authed_route_redirect' => 'home',
|
||||
];
|
||||
|
||||
37
config/platform/purchases/one_time.php
Normal file
37
config/platform/purchases/one_time.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => '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',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
];
|
||||
134
config/platform/purchases/subscriptions.php
Normal file
134
config/platform/purchases/subscriptions.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
// (Personal|Business) (Starter|Creator|Pro)
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => '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',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -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'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('google_id')->nullable()->unique();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('google_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('password')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('password')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscription_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
79
package-lock.json
generated
79
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<MittProvider>
|
||||
<AxiosProvider>
|
||||
<Toaster position="top-right" />
|
||||
<AuthDialog />
|
||||
<App {...props} />
|
||||
</AxiosProvider>
|
||||
</MittProvider>
|
||||
|
||||
46
resources/js/components/magicui/pulsating-button.tsx
Normal file
46
resources/js/components/magicui/pulsating-button.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PulsatingButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
pulseColor?: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
export const PulsatingButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
PulsatingButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
pulseColor = "#808080",
|
||||
duration = "1.5s",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center justify-center rounded-lg bg-primary px-4 py-2 text-center text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--pulse-color": pulseColor,
|
||||
"--duration": duration,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative z-10">{children}</div>
|
||||
<div className="absolute left-1/2 top-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-lg bg-inherit" />
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
PulsatingButton.displayName = "PulsatingButton";
|
||||
150
resources/js/components/magicui/sparkles-text.tsx
Normal file
150
resources/js/components/magicui/sparkles-text.tsx
Normal file
@@ -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<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
<motion.svg
|
||||
key={id}
|
||||
className="pointer-events-none absolute z-20"
|
||||
initial={{ opacity: 0, left: x, top: y }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0, scale, 0],
|
||||
rotate: [75, 120, 150],
|
||||
}}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
>
|
||||
<path
|
||||
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface SparklesTextProps {
|
||||
/**
|
||||
* @default <div />
|
||||
* @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<SparklesTextProps> = ({
|
||||
children,
|
||||
colors = { first: "#9E7AFF", second: "#FE8BBB" },
|
||||
className,
|
||||
sparklesCount = 10,
|
||||
...props
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={cn("text-6xl font-bold", className)}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
"--sparkles-first-color": `${colors.first}`,
|
||||
"--sparkles-second-color": `${colors.second}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="relative inline-block">
|
||||
{sparkles.map((sparkle) => (
|
||||
<Sparkle key={sparkle.id} {...sparkle} />
|
||||
))}
|
||||
<strong>{children}</strong>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
78
resources/js/modules/auth/AuthDialog.jsx
Normal file
78
resources/js/modules/auth/AuthDialog.jsx
Normal file
@@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">{isLogin ? 'Welcome back' : 'Create account'}</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
{isLogin ? 'Sign in to your account to continue' : 'Sign up to get started'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6">
|
||||
<Button variant="outline" className="bg-background hover:bg-accent h-12 w-full" type="button" onClick={() => handleGoogleLogin()}>
|
||||
<svg className="mr-3 h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
{isLogin ? 'Sign in with Google' : 'Sign up with Google'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-center text-xs">By continuing, you agree to our Terms of Service and Privacy Policy</div>
|
||||
|
||||
<div className="mt-4 text-center text-sm">
|
||||
{isLogin ? "Don't have an account?" : 'Already have an account?'}{' '}
|
||||
<Button variant="link" className="px-0 text-sm font-semibold" onClick={() => setIsLogin(!isLogin)}>
|
||||
{isLogin ? 'Sign up' : 'Sign in'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthDialog;
|
||||
@@ -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 = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<UpgradeSheet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) {
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="link">
|
||||
<SettingsIcon className="h-6 w-6" /> Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>Change your settings here.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid px-2">
|
||||
{/* {!auth.user && <Button variant="outline">Join Now</Button>}
|
||||
{!auth.user && <Button variant="link">Login</Button>} */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="genAlphaSlang"
|
||||
checked={getSetting('genAlphaSlang')}
|
||||
onCheckedChange={() => setSetting('genAlphaSlang', !getSetting('genAlphaSlang'))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="genAlphaSlang"
|
||||
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Use gen alpha slang
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="grid px-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.href = route('home');
|
||||
}}
|
||||
variant="link"
|
||||
>
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -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 (
|
||||
<div className={cn('flex w-full items-center justify-between rounded-xl bg-white p-2 shadow-sm dark:bg-neutral-700', className)}>
|
||||
@@ -19,33 +22,16 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
|
||||
|
||||
<h1 className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">MEMEAIGEN</h1>
|
||||
|
||||
<Button variant="outline" className="inline-flex gap-1 rounded" onClick={() => setOpenCoinDialog(true)}>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
<CoinIcon className="h-8 w-8" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="inline-flex gap-1 rounded"
|
||||
onClick={() => {
|
||||
openUpgradeSheet();
|
||||
}}
|
||||
>
|
||||
{/* <span className="text-sm font-semibold">0</span> */}
|
||||
<CartIcon className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
<Dialog open={openCoinDialog} onOpenChange={(open) => setOpenCoinDialog(open)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{getSetting('genAlphaSlang') ? 'Bruh' : 'Feature coming soon'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{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."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between gap-1">
|
||||
{/* <span class="text-muted-foreground text-xs italic">
|
||||
Note: You can turn {getSetting('genAlphaSlang') ? 'off' : 'on'} gen alpha slang in Settings.
|
||||
</span> */}
|
||||
<Button variant="outline" onClick={() => setOpenCoinDialog(false)}>
|
||||
{getSetting('genAlphaSlang') ? 'Bet' : 'Okay'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
103
resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx
Normal file
103
resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx
Normal file
@@ -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<CarouselApi>();
|
||||
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 (
|
||||
<div className="mx-auto w-full max-w-2xl space-y-4">
|
||||
<Carousel
|
||||
plugins={[
|
||||
Autoplay({
|
||||
delay: 5500,
|
||||
}),
|
||||
]}
|
||||
setApi={setApi}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent>
|
||||
{upgradePlanData.map((item, index) => {
|
||||
const IconComponent = item.icon;
|
||||
return (
|
||||
<CarouselItem key={index}>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="mx-auto mb-0 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<IconComponent className="h-10 w-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">{item.title}</h3>
|
||||
<p className="max-w-sm text-sm">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
{/* Centered Dot Navigation */}
|
||||
<div className="flex justify-center space-x-2">
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cn(
|
||||
'h-3 w-3 rounded-full transition-all duration-200 hover:scale-110',
|
||||
current === index + 1 ? 'bg-primary scale-110' : 'bg-muted-foreground/30 hover:bg-muted-foreground/50',
|
||||
)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradePlanCarousel;
|
||||
198
resources/js/modules/upgrade/upgrade-sheet.jsx
Normal file
198
resources/js/modules/upgrade/upgrade-sheet.jsx
Normal file
@@ -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. <br><br>This plan also includes a Personal license, which allows you to use the app for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the perfect choice for you.',
|
||||
},
|
||||
{
|
||||
q: 'Why are your plans extremely affordable for creators?',
|
||||
a: "We're glad you think this way! As creators ourselves, we know the journey of creating and monetizing your channel is not easy.<br><br> We believe that creators deserve access to high quality videos, and we're committed to making it accessible to every creator.<br><br>If one day you've made it to the top, don't forget us! We'd love to hear your growth stories.",
|
||||
},
|
||||
{
|
||||
q: 'Can I use the Personal Creator Plan for my business & commercial projects?',
|
||||
a: "A hard NO.<br><br> The Personal Creator Plan is designed for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the right choice for you. The Personal Creator Plan is designed to empower creators, helping them to monetize their channels and grow their audience. <br><br>However, if you are a business, we recommend you to use the Business Creator Plan (coming soon), which contains a Business License that you can use for commercial projects.<br><br>Contact us if you have an urgent need for this plan, and we'll see what can be done to help you.",
|
||||
},
|
||||
{
|
||||
q: 'How do I lock in my subscription price?',
|
||||
a: "Simple! Just subscribe to the plan before the next price change. We can't guarantee that you'll be able to purchase the same plan at the current price that you've locked in. But we'll do our best to make sure you get the best value for your money.",
|
||||
},
|
||||
{
|
||||
q: 'Is the $4/month pricing permanent?',
|
||||
a: 'Yes, the $4/month launch pricing is a special offer for our first 1000 users. After 1000 users, the plan will be priced at a higher rate. Lock in your launch pricing by subscribing now!',
|
||||
},
|
||||
{
|
||||
q: 'What can I do with credits?',
|
||||
a: 'You can use credits to access AI features such as AI caption generation & AI background generation.',
|
||||
},
|
||||
// {
|
||||
// q: 'What is the difference of purchasing credits from a subscription vs a one-time credit pack?',
|
||||
// a: 'When you purchase credits from a subscription, (e.g. 500 Credits/m) you get monthly allowance of 500 credits. Unused credits will not be carry forwarded to the next month.<br><br>When you purchase credits from a credit pack, (e.g. 500 Credit Pack) the credits will not expire and you can use them anytime.',
|
||||
// },
|
||||
{
|
||||
q: 'Can I cancel my subscription anytime?',
|
||||
a: "Yes! You can cancel your subscription at any time. Your plan will remain active until the end of your current billing period, and you'll retain access to all premium features during that time. Just know that you might not be able to purchase the same plan at the current price that you've locked in.",
|
||||
},
|
||||
{
|
||||
q: 'Do you offer refunds?',
|
||||
a: 'Yes, we offer refunds for subscription plans. Credits are non-refundable once purchased as they do not expire.',
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for open modal event
|
||||
const openModalListener = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// Register listener for opening the modal
|
||||
emitter.on('openUpgradeSheet', openModalListener);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
emitter.off('openUpgradeSheet', openModalListener);
|
||||
};
|
||||
}, [emitter]);
|
||||
|
||||
// Handle sheet state changes
|
||||
const handleOpenChange = (open) => {
|
||||
setIsOpen(open);
|
||||
|
||||
// If sheet is closing, emit the close event
|
||||
if (!open) {
|
||||
emitter.emit('closeUpgradeSheet');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = (subscription) => {
|
||||
checkoutSubscribe(subscription.stripe_monthly_price_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<SheetContent side="bottom" className="max-h-screen overflow-y-scroll pb-1">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center justify-center gap-2 sm:text-center">
|
||||
<CartIcon className={'h-4 w-4'} /> Store
|
||||
</SheetTitle>
|
||||
<SheetDescription className="hidden"> </SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mx-auto w-full max-w-[600px] space-y-3 px-4">
|
||||
{subscription ? (
|
||||
<div id="plan-purchase" className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
||||
<SparklesText className="text-xl font-bold sm:text-2xl">
|
||||
Upgrade to {subscription?.name} Plan<br></br> at only {subscription?.symbol}
|
||||
{subscription?.amount}
|
||||
{subscription?.primary_interval === 'month' ? '/m' : '/y'}*
|
||||
</SparklesText>
|
||||
<UpgradePlanCarousel />
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
disabled={isCheckingOut}
|
||||
onClick={() => {
|
||||
handleSubscribe(subscription);
|
||||
}}
|
||||
size="lg"
|
||||
className="mx-auto w-[220px] text-sm sm:text-base"
|
||||
>
|
||||
{isCheckingOut ? (
|
||||
<Spinner className="text-muted h-4 w-4" />
|
||||
) : (
|
||||
<span>
|
||||
Subscribe at ({subscription?.symbol}
|
||||
{subscription?.amount}
|
||||
{subscription?.primary_interval === 'month' ? '/m' : '/y'})*
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-muted-foreground text-xs">* Launch pricing limited to first 1000 users</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
isFetchingPricing && (
|
||||
<div className="mx-auto w-full">
|
||||
<Spinner className="h-6 w-6" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div id="credit-purchase" className="space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
||||
<div className="text-xl font-extrabold sm:text-2xl">Buy Credit Packs</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Unlock AI meme captions and backgrounds with credits. Perfect for overcoming creator's block and discovering new concepts.
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="grid">
|
||||
<div className="text inline-flex items-center font-semibold">
|
||||
<CoinIcon className="inline h-4 w-4 flex-shrink-0" />
|
||||
<span className="ml-1">500 Credit Pack</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs font-semibold break-words">
|
||||
Approx. 250 AI captions & 250 AI backgrounds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-muted-foreground text-sm">$4.00</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full flex-shrink-0 sm:w-auto">Buy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="faq" className="space-y-4 rounded-lg border p-4 sm:p-7">
|
||||
<div className="text-center text-xl font-extrabold sm:text-2xl">Frequently Asked Questions</div>
|
||||
<Accordion type="single" collapsible className="w-full" defaultValue="item-1">
|
||||
{faqData.map((faq, index) => (
|
||||
<AccordionItem key={index} value={`item-${index + 1}`}>
|
||||
<AccordionTrigger className="text-left text-sm break-words sm:text-base">{faq.q}</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<div className="overflow-wrap-anywhere text-sm break-words" dangerouslySetInnerHTML={{ __html: faq.a }} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
<SheetClose asChild className="hidden">
|
||||
<Button variant="outline">Close</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradeSheet;
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
48
resources/js/reusables/cart-icon.jsx
Normal file
48
resources/js/reusables/cart-icon.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
const CartIcon = ({ className }) => {
|
||||
return (
|
||||
<svg className={className} width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M56.4498 34.3344H120.029H128.613H137.196V8.58359C137.197 3.84375 133.353 0 128.613 0H47.8658C43.1252 0 39.2822 3.84375 39.2822 8.58359V34.3344H47.8658H56.4498Z"
|
||||
fill="#F28618"
|
||||
/>
|
||||
<path
|
||||
d="M162.948 28.3266C162.059 28.3266 161.202 28.1914 160.395 27.941C159.32 27.6067 158.336 27.0668 157.488 26.3668C155.58 24.7926 154.364 22.4098 154.364 19.743V34.0488H99.9998H14.1622C11.2872 34.0488 8.60287 35.4891 7.01186 37.8836C5.42162 40.2785 5.13529 43.311 6.25014 45.9613L39.9533 126.077C41.2927 129.261 44.4107 131.332 47.8654 131.332H99.9998H154.363V148.498H162.947C167.688 148.498 171.531 152.342 171.531 157.082V122.748V71.2445V28.3266H162.948Z"
|
||||
fill="#BDFDFF"
|
||||
/>
|
||||
<path
|
||||
d="M39.954 126.076C41.2935 129.261 44.4114 131.331 47.8661 131.331H154.364V148.498H162.948C167.688 148.498 171.531 152.341 171.531 157.082V122.747V71.2445V28.3266H162.948C158.207 28.3266 154.364 24.4832 154.364 19.743V34.0488H14.1626C11.2876 34.0488 8.60324 35.4891 7.01222 37.8836C5.42199 40.2789 5.13566 43.311 6.2505 45.9613L39.954 126.076Z"
|
||||
fill="#FFCC33"
|
||||
/>
|
||||
<path
|
||||
d="M162.948 28.3265H171.531H185.837C190.578 28.3265 194.421 24.4832 194.421 19.743C194.421 15.0031 190.578 11.1594 185.837 11.1594H162.947C158.207 11.1594 154.364 15.0031 154.364 19.743C154.364 24.4832 158.207 28.3265 162.948 28.3265Z"
|
||||
fill="#F28618"
|
||||
/>
|
||||
<path
|
||||
d="M162.948 148.497H154.364H134.335H66.2375H31.9023C27.1617 148.497 23.3187 152.341 23.3187 157.081C23.3187 161.821 27.1621 165.664 31.9023 165.664H41.9625H51.3718H66.2371H81.1023H90.5117H110.06H119.469H134.334H149.2H158.609H162.946C167.687 165.664 171.53 161.821 171.53 157.081C171.53 152.341 167.688 148.497 162.948 148.497Z"
|
||||
fill="#F28618"
|
||||
/>
|
||||
<path
|
||||
d="M125.752 174.249C125.752 178.982 129.602 182.832 134.335 182.832C139.068 182.832 142.919 178.982 142.919 174.249C142.919 169.516 139.068 165.665 134.335 165.665C129.602 165.665 125.752 169.516 125.752 174.249Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M57.6534 174.249C57.6534 178.982 61.5042 182.832 66.237 182.832C70.9702 182.832 74.8206 178.982 74.8206 174.249C74.8206 169.516 70.9698 165.665 66.237 165.665C61.5042 165.665 57.6534 169.516 57.6534 174.249Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M134.335 165.665C139.068 165.665 142.919 169.516 142.919 174.248C142.919 178.981 139.068 182.832 134.335 182.832C129.602 182.832 125.752 178.981 125.752 174.248C125.752 169.516 129.602 165.665 134.335 165.665H119.47H110.061C109.108 168.352 108.584 171.24 108.584 174.248C108.584 188.448 120.136 200 134.335 200C148.534 200 160.086 188.448 160.086 174.248C160.086 171.239 159.563 168.352 158.61 165.665H149.2H134.335Z"
|
||||
fill="#898890"
|
||||
/>
|
||||
<path
|
||||
d="M66.2374 165.665C70.9706 165.665 74.821 169.516 74.821 174.248C74.821 178.981 70.9702 182.832 66.2374 182.832C61.5046 182.832 57.6538 178.981 57.6538 174.248C57.6538 169.516 61.5034 165.665 66.2374 165.665H51.3721H41.9628C41.01 168.352 40.4862 171.24 40.4862 174.248C40.4862 188.448 52.0382 200 66.2374 200C80.4366 200 91.9886 188.448 91.9886 174.248C91.9886 171.239 91.4651 168.352 90.512 165.665H81.1026H66.2374Z"
|
||||
fill="#57565C"
|
||||
/>
|
||||
<path
|
||||
d="M162.948 28.3266C158.207 28.3266 154.364 24.4832 154.364 19.743V34.0488H100V131.332H154.364V148.498H162.947C167.688 148.498 171.531 152.342 171.531 157.082V122.748V71.2445V28.3266H162.948Z"
|
||||
fill="#FFE14D"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartIcon;
|
||||
@@ -1,10 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CoinIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CoinIcon: React.FC<CoinIconProps> = ({ className }) => {
|
||||
const CoinIcon = ({ className }) => {
|
||||
return (
|
||||
<svg className={className} width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_23_2)">
|
||||
95
resources/js/stores/PricingStore.js
Normal file
95
resources/js/stores/PricingStore.js
Normal file
@@ -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;
|
||||
File diff suppressed because one or more lines are too long
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user