Update
This commit is contained in:
212
app/Helpers/FirstParty/Credits/CreditsHelper.php
Normal file
212
app/Helpers/FirstParty/Credits/CreditsHelper.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers\FirstParty\Credits;
|
||||||
|
|
||||||
|
use App\Helpers\FirstParty\Purchase\PurchaseHelper;
|
||||||
|
use App\Models\UserCredit;
|
||||||
|
use App\Models\UserCreditTransaction;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CreditsHelper
|
||||||
|
{
|
||||||
|
public static function getCreditPackByStripePriceId($stripe_price_id)
|
||||||
|
{
|
||||||
|
$credit_packs = self::getCreditPacks(null, true);
|
||||||
|
|
||||||
|
foreach ($credit_packs as $credit_pack) {
|
||||||
|
|
||||||
|
$stripe_price_ids = PurchaseHelper::getPlanSystemProperty($credit_pack, 'stripe.stripe_price_ids');
|
||||||
|
|
||||||
|
if (in_array($stripe_price_id, $stripe_price_ids)) {
|
||||||
|
return $credit_pack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCreditsPackById($id, $enabled = true, $system = false)
|
||||||
|
{
|
||||||
|
$credits_packs = self::getCreditPacks($enabled, $system);
|
||||||
|
foreach ($credits_packs as $credits_pack) {
|
||||||
|
if ($credits_pack['id'] == $id) {
|
||||||
|
return $credits_pack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCreditPacks($enabled = null, $system = true)
|
||||||
|
{
|
||||||
|
|
||||||
|
$credit_packs = [];
|
||||||
|
|
||||||
|
$one_times = PurchaseHelper::getOneTimePurchases($enabled, $system);
|
||||||
|
|
||||||
|
foreach ($one_times as $one_time) {
|
||||||
|
|
||||||
|
if ($one_time['type'] == 'alacarte_credits') {
|
||||||
|
$credit_packs[] = $one_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $credit_packs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ////
|
||||||
|
|
||||||
|
public static function getUserCredits(int $user_id): UserCredit
|
||||||
|
{
|
||||||
|
$user_credit = UserCredit::where('user_id', $user_id)->first();
|
||||||
|
|
||||||
|
if (! $user_credit) {
|
||||||
|
$user_credit = UserCredit::create([
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'subscription_credits' => 0,
|
||||||
|
'alacarte_credits' => 0,
|
||||||
|
'spend_subscription_first' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user_credit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when user purchase annual subscription, need to 12x the deposit amount
|
||||||
|
public static function deposit(int $user_id, int $amount, string $type, ?string $description = null): bool
|
||||||
|
{
|
||||||
|
if ($amount <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Amount must be positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($type, [UserCredit::TYPE_SUBSCRIPTION, UserCredit::TYPE_ALACARTE])) {
|
||||||
|
throw new \InvalidArgumentException('Type must be subscription or alacarte');
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($user_id, $amount, $type, $description) {
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
|
||||||
|
if ($type === UserCredit::TYPE_SUBSCRIPTION) {
|
||||||
|
$user_credit->subscription_credits += $amount;
|
||||||
|
} else {
|
||||||
|
$user_credit->alacarte_credits += $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_credit->save();
|
||||||
|
self::logTransaction($user_id, $type, 'deposit', $amount, $user_credit, $description);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function spend(int $user_id, int $amount, ?string $description = null, ?bool $use_subscription_first = null): bool
|
||||||
|
{
|
||||||
|
if ($amount <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Amount must be positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! self::canSpend($user_id, $amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($user_id, $amount, $description, $use_subscription_first) {
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
$remaining = $amount;
|
||||||
|
$subscription_first = $use_subscription_first ?? $user_credit->spend_subscription_first;
|
||||||
|
|
||||||
|
$spend_order = $subscription_first
|
||||||
|
? [UserCredit::TYPE_SUBSCRIPTION, UserCredit::TYPE_ALACARTE]
|
||||||
|
: [UserCredit::TYPE_ALACARTE, UserCredit::TYPE_SUBSCRIPTION];
|
||||||
|
|
||||||
|
foreach ($spend_order as $type) {
|
||||||
|
if ($remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = $type === UserCredit::TYPE_SUBSCRIPTION
|
||||||
|
? $user_credit->subscription_credits
|
||||||
|
: $user_credit->alacarte_credits;
|
||||||
|
|
||||||
|
if ($available > 0) {
|
||||||
|
$to_spend = min($remaining, $available);
|
||||||
|
|
||||||
|
if ($type === UserCredit::TYPE_SUBSCRIPTION) {
|
||||||
|
$user_credit->subscription_credits -= $to_spend;
|
||||||
|
} else {
|
||||||
|
$user_credit->alacarte_credits -= $to_spend;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::logTransaction($user_id, $type, 'spend', -$to_spend, $user_credit, $description);
|
||||||
|
$remaining -= $to_spend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_credit->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canSpend(int $user_id, int $amount): bool
|
||||||
|
{
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
|
||||||
|
return self::_getTotalBalance($user_credit) >= $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function summary(int $user_id): array
|
||||||
|
{
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'subscription' => $user_credit->subscription_credits,
|
||||||
|
'alacarte' => $user_credit->alacarte_credits,
|
||||||
|
'total' => self::_getTotalBalance($user_credit),
|
||||||
|
'preference' => $user_credit->spend_subscription_first ? 'subscription_first' : 'alacarte_first',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function balance(int $user_id, ?string $type = null): int
|
||||||
|
{
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
UserCredit::TYPE_SUBSCRIPTION => $user_credit->subscription_credits,
|
||||||
|
UserCredit::TYPE_ALACARTE => $user_credit->alacarte_credits,
|
||||||
|
default => self::_getTotalBalance($user_credit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function _getTotalBalance(UserCredit $user_credit): int
|
||||||
|
{
|
||||||
|
return $user_credit->subscription_credits + $user_credit->alacarte_credits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setSpendSubscriptionFirst(int $user_id, bool $spendSubscriptionFirst): bool
|
||||||
|
{
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
$user_credit->spend_subscription_first = $spendSubscriptionFirst;
|
||||||
|
|
||||||
|
return $user_credit->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function transactionHistory(int $user_id, int $limit = 20): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
$user_credit = self::getUserCredits($user_id);
|
||||||
|
|
||||||
|
return $user_credit->transactions()->limit($limit)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function logTransaction(int $user_id, string $type, string $operation, int $amount, UserCredit $user_credit, ?string $description = null): void
|
||||||
|
{
|
||||||
|
UserCreditTransaction::create([
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'credit_type' => $type,
|
||||||
|
'operation' => $operation,
|
||||||
|
'amount' => $amount,
|
||||||
|
'subscription_balance_after' => $user_credit->subscription_credits,
|
||||||
|
'alacarte_balance_after' => $user_credit->alacarte_credits,
|
||||||
|
'description' => $description,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Helpers/FirstParty/Credits/CreditsService.php
Normal file
48
app/Helpers/FirstParty/Credits/CreditsService.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers\FirstParty\Credits;
|
||||||
|
|
||||||
|
use App\Models\UserCredit;
|
||||||
|
|
||||||
|
class CreditsService
|
||||||
|
{
|
||||||
|
public static function depositSubscription(int $userId, int $amount, ?string $description = null): bool
|
||||||
|
{
|
||||||
|
return CreditsHelper::deposit($userId, $amount, UserCredit::TYPE_SUBSCRIPTION, $description);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function depositAlacarte(int $userId, int $amount, ?string $description = null): bool
|
||||||
|
{
|
||||||
|
return CreditsHelper::deposit($userId, $amount, UserCredit::TYPE_ALACARTE, $description);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function spend(int $userId, int $amount, ?string $description = null, ?bool $overrideSubscriptionFirst = null): bool
|
||||||
|
{
|
||||||
|
return CreditsHelper::spend($userId, $amount, $description, $overrideSubscriptionFirst);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setSpendSubscriptionFirst(int $userId, bool $spendSubscriptionFirst): bool
|
||||||
|
{
|
||||||
|
return CreditsHelper::setSpendSubscriptionFirst($userId, $spendSubscriptionFirst);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function balance(int $userId, ?string $type = null): int
|
||||||
|
{
|
||||||
|
return CreditsHelper::balance($userId, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canSpend(int $userId, int $amount): bool
|
||||||
|
{
|
||||||
|
return CreditsHelper::canSpend($userId, $amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function summary(int $userId): array
|
||||||
|
{
|
||||||
|
return CreditsHelper::summary($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function transactionHistory(int $userId, int $limit = 20): \Illuminate\Database\Eloquent\Collection
|
||||||
|
{
|
||||||
|
return CreditsHelper::transactionHistory($userId, $limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Helpers/FirstParty/Credits/UserCreditHelper.php
Normal file
67
app/Helpers/FirstParty/Credits/UserCreditHelper.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers\FirstParty\Credits;
|
||||||
|
|
||||||
|
use App\Helpers\FirstParty\Stripe\StripeHelper;
|
||||||
|
use App\Models\PaymentWebhookProcessedLog;
|
||||||
|
use Laravel\Cashier\Events\WebhookReceived;
|
||||||
|
|
||||||
|
class UserCreditHelper
|
||||||
|
{
|
||||||
|
public static function handleStripeWebhook(WebhookReceived $event)
|
||||||
|
{
|
||||||
|
$payload = $event->payload;
|
||||||
|
|
||||||
|
switch ($payload['type']) {
|
||||||
|
case 'checkout.session.completed':
|
||||||
|
self::handleCheckoutSessionCompleted($payload['data']['object']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function handleCheckoutSessionCompleted($checkout_session)
|
||||||
|
{
|
||||||
|
// Check if the webhook has already been processed
|
||||||
|
$payment_webhook_processed_log = PaymentWebhookProcessedLog::where('provider', 'stripe')
|
||||||
|
->where('log_id', $checkout_session['id'])
|
||||||
|
->where('log_type', 'checkout_session')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! is_null($payment_webhook_processed_log)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user associated with the checkout session
|
||||||
|
$user = StripeHelper::getUserByStripeCustomerId($checkout_session['customer']);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
|
||||||
|
// Get the line items from the checkout session
|
||||||
|
$lineItems = \Stripe\Checkout\Session::allLineItems($checkout_session['id']);
|
||||||
|
|
||||||
|
foreach ($lineItems->data as $lineItem) {
|
||||||
|
$stripe_price_id = $lineItem->price->id;
|
||||||
|
$stripe_product_id = $lineItem->price->product;
|
||||||
|
|
||||||
|
// Get the credit pack associated with the price ID
|
||||||
|
$credit_pack = CreditsHelper::getCreditPackByStripePriceId($stripe_product_id, $stripe_price_id, get_stripe_env());
|
||||||
|
|
||||||
|
if (is_null($credit_pack)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_credits = $credit_pack['purchased_credits'] + $credit_pack['bonus_credits'];
|
||||||
|
|
||||||
|
// Deposit the credits to the user's account
|
||||||
|
CreditsService::depositAlacarte($user->id, $total_credits, "Purchased {$credit_pack['name']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the webhook as processed
|
||||||
|
PaymentWebhookProcessedLog::create([
|
||||||
|
'provider' => 'stripe',
|
||||||
|
'log_id' => $checkout_session['id'],
|
||||||
|
'log_type' => 'checkout_session',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Helpers/FirstParty/Purchase/CreditsPurchaseHelper.php
Normal file
73
app/Helpers/FirstParty/Purchase/CreditsPurchaseHelper.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers\FirstParty\Purchase;
|
||||||
|
|
||||||
|
use App\Helpers\FirstParty\Credits\CreditsHelper;
|
||||||
|
use App\Helpers\FirstParty\Credits\CreditsService;
|
||||||
|
use App\Helpers\FirstParty\Stripe\StripeHelper;
|
||||||
|
use App\Models\PaymentWebhookProcessedLog;
|
||||||
|
use Laravel\Cashier\Events\WebhookReceived;
|
||||||
|
|
||||||
|
class CreditsPurchaseHelper
|
||||||
|
{
|
||||||
|
public static function handleWebhookEvents(WebhookReceived $event)
|
||||||
|
{
|
||||||
|
|
||||||
|
switch (StripeHelper::getEventType($event)) {
|
||||||
|
case 'checkout.session.completed':
|
||||||
|
self::handleCheckoutSessionCompleted($event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function handleCheckoutSessionCompleted($event)
|
||||||
|
{
|
||||||
|
|
||||||
|
$object = StripeHelper::getEventObject($event);
|
||||||
|
|
||||||
|
// Check if the webhook has already been processed
|
||||||
|
$payment_webhook_processed_log = PaymentWebhookProcessedLog::where('provider', 'stripe')
|
||||||
|
->where('log_id', $object['id'])
|
||||||
|
->where('log_type', 'checkout_session')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! is_null($payment_webhook_processed_log)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user associated with the checkout session
|
||||||
|
$user = StripeHelper::getUserByStripeID($object['customer']);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
|
||||||
|
// Get the line items from the checkout session
|
||||||
|
StripeHelper::setStripeApiKey();
|
||||||
|
$lineItems = \Stripe\Checkout\Session::allLineItems($object['id']);
|
||||||
|
|
||||||
|
// dd($lineItems);
|
||||||
|
|
||||||
|
foreach ($lineItems->data as $lineItem) {
|
||||||
|
$stripe_price_id = $lineItem->price->id;
|
||||||
|
|
||||||
|
// Get the credit pack associated with the price ID
|
||||||
|
$credit_pack = CreditsHelper::getCreditPackByStripePriceId($stripe_price_id);
|
||||||
|
|
||||||
|
if (is_null($credit_pack)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_credits = PurchaseHelper::getPlanSystemProperty($credit_pack, 'credits');
|
||||||
|
|
||||||
|
// Deposit the credits to the user's account
|
||||||
|
CreditsService::depositAlacarte($user->id, $total_credits, "Purchased {$credit_pack['name']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the webhook as processed
|
||||||
|
PaymentWebhookProcessedLog::create([
|
||||||
|
'provider' => 'stripe',
|
||||||
|
'log_id' => $object['id'],
|
||||||
|
'log_type' => 'checkout_session',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public static function getPricingPageSubscriptions()
|
|||||||
|
|
||||||
$subscriptions = self::filterShowInPricing($subscriptions);
|
$subscriptions = self::filterShowInPricing($subscriptions);
|
||||||
|
|
||||||
$subscriptions = self::setSystemPlan($subscriptions, 'stripe.current_stripe_price_id.month');
|
$subscriptions = self::setSystemPlan($subscriptions, 'stripe.current_stripe_price_id.month', 'stripe_monthly_price_id');
|
||||||
|
|
||||||
$subscriptions = self::filterUnsetSystem($subscriptions);
|
$subscriptions = self::filterUnsetSystem($subscriptions);
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public static function getPricingPageOneTime()
|
|||||||
|
|
||||||
$one_time = PurchaseHelper::filterShowInPricing($one_time);
|
$one_time = PurchaseHelper::filterShowInPricing($one_time);
|
||||||
|
|
||||||
$one_time = PurchaseHelper::setSystemPlan($one_time, 'stripe.current_stripe_price_id');
|
$one_time = PurchaseHelper::setSystemPlan($one_time, 'stripe.current_stripe_price_id', 'stripe_price_id');
|
||||||
|
|
||||||
$one_time = PurchaseHelper::filterUnsetSystem($one_time);
|
$one_time = PurchaseHelper::filterUnsetSystem($one_time);
|
||||||
|
|
||||||
@@ -136,11 +136,11 @@ public static function getOneTimePurchases(?bool $enabled, $system = false)
|
|||||||
return $tmp_otp;
|
return $tmp_otp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function setSystemPlan($arr, $property)
|
public static function setSystemPlan($arr, $property, $key_name)
|
||||||
{
|
{
|
||||||
foreach ($arr as $key => $item) {
|
foreach ($arr as $key => $item) {
|
||||||
|
|
||||||
$arr[$key]['stripe_monthly_price_id'] = PurchaseHelper::getPlanSystemProperty($item, $property);
|
$arr[$key][$key_name] = PurchaseHelper::getPlanSystemProperty($item, $property);
|
||||||
|
|
||||||
// $stripe_monthly_price_id = PurchaseHelper::getPlanSystemProperty($subscription, 'stripe.product_id.month');
|
// $stripe_monthly_price_id = PurchaseHelper::getPlanSystemProperty($subscription, 'stripe.product_id.month');
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ public static function filterShowInPricing($arr)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getStripeEnvironment()
|
public static function getStripeEnvironment()
|
||||||
{
|
{
|
||||||
$env = App::environment();
|
$env = App::environment();
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
class SubscriptionHelper
|
class SubscriptionHelper
|
||||||
{
|
{
|
||||||
public static function handleSubscriptionWebhookEvents(WebhookReceived $event)
|
public static function handleWebhookEvents(WebhookReceived $event)
|
||||||
{
|
{
|
||||||
switch ($event->payload['type']) {
|
switch (StripeHelper::getEventType($event)) {
|
||||||
case 'customer.subscription.created':
|
case 'customer.subscription.created':
|
||||||
case 'customer.subscription.updated':
|
case 'customer.subscription.updated':
|
||||||
self::handleSubscriptionUpsert($event);
|
self::handleSubscriptionUpsert($event);
|
||||||
@@ -25,7 +25,7 @@ public static function handleSubscriptionWebhookEvents(WebhookReceived $event)
|
|||||||
|
|
||||||
private static function handleSubscriptionUpsert(WebhookReceived $event)
|
private static function handleSubscriptionUpsert(WebhookReceived $event)
|
||||||
{
|
{
|
||||||
$object = $event->payload['data']['object'];
|
$object = StripeHelper::getEventObject($event);
|
||||||
|
|
||||||
// dump($object);
|
// dump($object);
|
||||||
|
|
||||||
@@ -83,6 +83,34 @@ private static function handleSubscriptionUpsert(WebhookReceived $event)
|
|||||||
|
|
||||||
private static function handleSubscriptionDelete(WebhookReceived $event)
|
private static function handleSubscriptionDelete(WebhookReceived $event)
|
||||||
{
|
{
|
||||||
// /
|
$object = StripeHelper::getEventObject($event);
|
||||||
|
|
||||||
|
$user = StripeHelper::getUserByStripeID($object['customer']);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
|
||||||
|
$plan = Plan::where('tier', 'free')->first();
|
||||||
|
|
||||||
|
if ($plan) {
|
||||||
|
$user_plan = UserPlan::where('user_id', $user->id)->first();
|
||||||
|
|
||||||
|
if ($user_plan) {
|
||||||
|
$user_plan->update([
|
||||||
|
'plan_id' => $plan->id,
|
||||||
|
'current_period_end' => null,
|
||||||
|
'cancel_at' => null,
|
||||||
|
'canceled_at' => null,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$user_plan = UserPlan::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'plan_id' => $plan->id,
|
||||||
|
'current_period_end' => null,
|
||||||
|
'cancel_at' => null,
|
||||||
|
'canceled_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Helpers\FirstParty\Stripe;
|
namespace App\Helpers\FirstParty\Stripe;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Laravel\Cashier\Events\WebhookReceived;
|
||||||
|
|
||||||
class StripeHelper
|
class StripeHelper
|
||||||
{
|
{
|
||||||
@@ -10,4 +11,19 @@ public static function getUserByStripeID($customer_id)
|
|||||||
{
|
{
|
||||||
return User::where('stripe_id', $customer_id)->first();
|
return User::where('stripe_id', $customer_id)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getEventObject(WebhookReceived $event)
|
||||||
|
{
|
||||||
|
return $event->payload['data']['object'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getEventType(WebhookReceived $event)
|
||||||
|
{
|
||||||
|
return $event->payload['type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setStripeApiKey()
|
||||||
|
{
|
||||||
|
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Helpers\FirstParty\Credits\CreditsService;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class UserAccountController extends Controller
|
class UserAccountController extends Controller
|
||||||
@@ -19,8 +20,8 @@ public function index()
|
|||||||
'user' => $user,
|
'user' => $user,
|
||||||
'billing' => [
|
'billing' => [
|
||||||
'provider' => 'stripe',
|
'provider' => 'stripe',
|
||||||
'portal' => Auth::user()->billingPortalUrl(route('home'))
|
],
|
||||||
]
|
'credits' => CreditsService::balance(Auth::id()),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
|
|
||||||
class UserPurchaseController extends Controller
|
class UserPurchaseController extends Controller
|
||||||
{
|
{
|
||||||
|
public function billingPortal(Request $request)
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'success' => [
|
||||||
|
'data' => [
|
||||||
|
'redirect' => Auth::user()->billingPortalUrl(route('home')),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function pricingPage(Request $request)
|
public function pricingPage(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -61,7 +72,7 @@ public function subscribeSuccess(Request $request)
|
|||||||
|
|
||||||
Session::forget('checkout_session_id');
|
Session::forget('checkout_session_id');
|
||||||
|
|
||||||
return redirect()->route('home')->with('success', 'Thank you for subscribing! Your subscription should be active momentarily. Please refresh the page if you do not see your plan.');
|
return redirect()->route('home')->with('success', 'Purchase successful! Your purchase should be active momentarily. Please refresh if you did not see any changes.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscribeCancelled(Request $request)
|
public function subscribeCancelled(Request $request)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Helpers\FirstParty\Purchase\CreditsPurchaseHelper;
|
||||||
use App\Helpers\FirstParty\Purchase\SubscriptionHelper;
|
use App\Helpers\FirstParty\Purchase\SubscriptionHelper;
|
||||||
use App\Helpers\FirstParty\Purchase\WatermarkUsageHelper;
|
use App\Helpers\FirstParty\Purchase\WatermarkUsageHelper;
|
||||||
use Laravel\Cashier\Events\WebhookReceived;
|
use Laravel\Cashier\Events\WebhookReceived;
|
||||||
@@ -21,7 +22,8 @@ public function __construct()
|
|||||||
*/
|
*/
|
||||||
public function handle(WebhookReceived $event): void
|
public function handle(WebhookReceived $event): void
|
||||||
{
|
{
|
||||||
SubscriptionHelper::handleSubscriptionWebhookEvents($event);
|
SubscriptionHelper::handleWebhookEvents($event);
|
||||||
|
CreditsPurchaseHelper::handleWebhookEvents($event);
|
||||||
WatermarkUsageHelper::handleWatermarkUsageWebhookEvents($event);
|
WatermarkUsageHelper::handleWatermarkUsageWebhookEvents($event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/Models/PaymentWebhookProcessedLog.php
Normal file
16
app/Models/PaymentWebhookProcessedLog.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentWebhookProcessedLog extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'payment_webhook_processed_logs';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'provider',
|
||||||
|
'log_id',
|
||||||
|
'log_type',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ class User extends Authenticatable
|
|||||||
protected function ids(): Attribute
|
protected function ids(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn($value, $attributes) => hashids_encode($attributes['id']),
|
get: fn ($value, $attributes) => hashids_encode($attributes['id']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
app/Models/UserCredit.php
Normal file
39
app/Models/UserCredit.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class UserCredit extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
const TYPE_SUBSCRIPTION = 'subscription';
|
||||||
|
|
||||||
|
const TYPE_ALACARTE = 'alacarte';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'subscription_credits',
|
||||||
|
'alacarte_credits',
|
||||||
|
'spend_subscription_first',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'spend_subscription_first' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transactions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserCreditTransaction::class, 'user_id', 'user_id')
|
||||||
|
->orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/Models/UserCreditTransaction.php
Normal file
57
app/Models/UserCreditTransaction.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UserCreditTransaction extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'credit_type',
|
||||||
|
'operation',
|
||||||
|
'amount',
|
||||||
|
'subscription_balance_after',
|
||||||
|
'alacarte_balance_after',
|
||||||
|
'description',
|
||||||
|
'metadata',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userCredit(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserCredit::class, 'user_id', 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeDeposits($query)
|
||||||
|
{
|
||||||
|
return $query->where('operation', 'deposit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeSpending($query)
|
||||||
|
{
|
||||||
|
return $query->where('operation', 'spend');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCredit(): bool
|
||||||
|
{
|
||||||
|
return $this->amount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDebit(): bool
|
||||||
|
{
|
||||||
|
return $this->amount < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ class UserUsage extends Model
|
|||||||
protected function ids(): Attribute
|
protected function ids(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn($value, $attributes) => hashids_encode($attributes['id']),
|
get: fn ($value, $attributes) => hashids_encode($attributes['id']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
'show_in_pricing' => true,
|
'show_in_pricing' => true,
|
||||||
'type' => 'alacarte_credits',
|
'type' => 'alacarte_credits',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'name' => '500 Credit Packs',
|
'name' => '500 Credit Pack',
|
||||||
'description' => 'Use credits to automatically generate captions & backgrounds for your memes.',
|
'description' => 'Approx. 250 AI captions & 250 AI backgrounds',
|
||||||
'amount' => 4,
|
'amount' => 4,
|
||||||
'currency' => 'usd',
|
'currency' => 'usd',
|
||||||
'symbol' => '$',
|
'symbol' => '$',
|
||||||
|
|||||||
@@ -53,4 +53,10 @@
|
|||||||
'redirect' => env('GOOGLE_CLIENT_REDIRECT_URI'),
|
'redirect' => env('GOOGLE_CLIENT_REDIRECT_URI'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'stripe' => [
|
||||||
|
'key' => env('STRIPE_KEY'),
|
||||||
|
'secret' => env('STRIPE_SECRET'),
|
||||||
|
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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('user_credits', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('subscription_credits')->default(0);
|
||||||
|
$table->integer('alacarte_credits')->default(0);
|
||||||
|
$table->boolean('spend_subscription_first')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_credits');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?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('user_credit_transactions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->enum('credit_type', ['subscription', 'alacarte']);
|
||||||
|
$table->enum('operation', ['deposit', 'spend']);
|
||||||
|
$table->integer('amount'); // positive for deposits, negative for spending
|
||||||
|
$table->integer('subscription_balance_after');
|
||||||
|
$table->integer('alacarte_balance_after');
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['user_id', 'created_at']);
|
||||||
|
$table->index(['user_id', 'credit_type']);
|
||||||
|
$table->index(['operation', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_credit_transactions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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('payment_webhook_processed_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->enum('provider', ['stripe']);
|
||||||
|
$table->string('log_id');
|
||||||
|
$table->enum('log_type', ['checkout_session']);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['log_id', 'log_type', 'provider'], 'idx_webhook_logs_search');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('payment_webhook_processed_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -16,12 +16,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import UpgradePlanCarousel from './partials/upgrade-plan-carousel.tsx';
|
import UpgradePlanCarousel from './partials/upgrade-plan-carousel.tsx';
|
||||||
|
|
||||||
const UpgradeSheet = () => {
|
const UpgradeSheet = () => {
|
||||||
const { subscription, one_times, isFetchingPricing, fetchPricing, isCheckingOut, checkoutSubscribe } = usePricingStore();
|
const { subscription, one_times, isFetchingPricing, fetchPricing, isCheckingOut, checkoutSubscribe, checkoutPurchase } = usePricingStore();
|
||||||
const { plan, billing, user_usage } = useUserStore();
|
const { plan, billing, user_usage, credits, redirectBillingPortal, isRedirectingToBilling } = useUserStore();
|
||||||
const { auth } = usePage().props;
|
const { auth } = usePage().props;
|
||||||
|
|
||||||
const [isRedirectingToBilling, setIsRedirectingToBilling] = useState(false);
|
|
||||||
|
|
||||||
// State to control sheet visibility
|
// State to control sheet visibility
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -101,6 +99,14 @@ const UpgradeSheet = () => {
|
|||||||
checkoutSubscribe(subscription.stripe_monthly_price_id);
|
checkoutSubscribe(subscription.stripe_monthly_price_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePurchaseOneTime = (one_time) => {
|
||||||
|
checkoutPurchase(one_time.stripe_price_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRedirectBillingPortal = () => {
|
||||||
|
redirectBillingPortal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
|
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
<SheetContent side="bottom" className="max-h-screen overflow-y-scroll pb-1">
|
<SheetContent side="bottom" className="max-h-screen overflow-y-scroll pb-1">
|
||||||
@@ -131,17 +137,19 @@ const UpgradeSheet = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Credits */}
|
{/* Credits */}
|
||||||
<div className="bg-card relative overflow-hidden rounded-xl border p-6 shadow-sm">
|
{credits != null && (
|
||||||
<div className="relative z-10">
|
<div className="bg-card relative overflow-hidden rounded-xl border p-6 shadow-sm">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="relative z-10">
|
||||||
<CoinIcon className="text-primary h-5 w-5" />
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<span className="text-muted-foreground text-xs font-medium">Credits</span>
|
<CoinIcon className="text-primary h-5 w-5" />
|
||||||
|
<span className="text-muted-foreground text-xs font-medium">Credits</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground text-xl font-bold">{credits}</div>
|
||||||
|
<div className="text-muted-foreground text-sm">available</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-foreground text-xl font-bold">999</div>
|
<CoinIcon className="bg-muted absolute -top-2 -right-2 h-20 w-20 opacity-15" />
|
||||||
<div className="text-muted-foreground text-sm">available</div>
|
|
||||||
</div>
|
</div>
|
||||||
<CoinIcon className="bg-muted absolute -top-2 -right-2 h-20 w-20 opacity-15" />
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -159,18 +167,15 @@ const UpgradeSheet = () => {
|
|||||||
<div className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
<div className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
||||||
<SparklesText className="text-xl font-bold sm:text-2xl">You're now in the {plan?.name} plan!</SparklesText>
|
<SparklesText className="text-xl font-bold sm:text-2xl">You're now in the {plan?.name} plan!</SparklesText>
|
||||||
|
|
||||||
{billing?.portal && (
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
onClick={() => {
|
||||||
onClick={() => {
|
handleRedirectBillingPortal();
|
||||||
setIsRedirectingToBilling(true);
|
}}
|
||||||
window.location.replace(billing?.portal);
|
disabled={isRedirectingToBilling}
|
||||||
}}
|
>
|
||||||
disabled={isRedirectingToBilling}
|
{isRedirectingToBilling ? <Spinner className="text-muted-foreground h-4 w-4" /> : 'Manage Subscription'}
|
||||||
>
|
</Button>
|
||||||
{isRedirectingToBilling ? <Spinner className="text-muted-foreground h-4 w-4" /> : 'Manage Subscription'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-muted-foreground block rounded border p-3 text-center text-xs">
|
<div className="text-muted-foreground block rounded border p-3 text-center text-xs">
|
||||||
<ShieldIcon className="mr-1 mb-1 inline h-4 w-4" />
|
<ShieldIcon className="mr-1 mb-1 inline h-4 w-4" />
|
||||||
@@ -229,24 +234,38 @@ const UpgradeSheet = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<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">
|
{one_times.map((one_time, index) => (
|
||||||
<div className="min-w-0 flex-1 space-y-1">
|
<div
|
||||||
<div className="grid">
|
key={one_time.id}
|
||||||
<div className="text inline-flex items-center font-semibold">
|
className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between"
|
||||||
<CoinIcon className="inline h-4 w-4 flex-shrink-0" />
|
>
|
||||||
<span className="ml-1">500 Credit Pack</span>
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
|
<div className="grid text-start">
|
||||||
|
<div className="text inline-flex items-center font-semibold">
|
||||||
|
<CoinIcon className="inline h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="ml-1">{one_time.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs font-semibold break-words">{one_time.description}</div>
|
||||||
</div>
|
</div>
|
||||||
<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="flex items-center gap-1">
|
||||||
<div className="text-muted-foreground text-sm">$4.00</div>
|
<div className="text-muted-foreground text-sm">
|
||||||
|
{one_time.symbol}
|
||||||
|
{one_time.amount} per pack
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
disabled={isCheckingOut}
|
||||||
|
onClick={() => {
|
||||||
|
handlePurchaseOneTime(one_time);
|
||||||
|
}}
|
||||||
|
className="w-full flex-shrink-0 sm:w-auto"
|
||||||
|
>
|
||||||
|
{isCheckingOut ? <Spinner className="text-muted h-4 w-4" /> : <span>Buy</span>}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full flex-shrink-0 sm:w-auto">Buy</Button>
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,33 @@ const usePricingStore = create(
|
|||||||
|
|
||||||
isCheckingOut: false,
|
isCheckingOut: false,
|
||||||
|
|
||||||
|
checkoutPurchase: async (price_id) => {
|
||||||
|
console.log('checkoutPurchase', price_id);
|
||||||
|
|
||||||
|
set({ isCheckingOut: true });
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(route('api.user.purchase'), { price_id: price_id });
|
||||||
|
console.log(response);
|
||||||
|
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.purchase'));
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
checkoutSubscribe: async (price_id) => {
|
checkoutSubscribe: async (price_id) => {
|
||||||
console.log('checkoutSubscribe', price_id);
|
console.log('checkoutSubscribe', price_id);
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,28 @@ const useUserStore = create(
|
|||||||
tier: 'free',
|
tier: 'free',
|
||||||
},
|
},
|
||||||
billing: null,
|
billing: null,
|
||||||
|
credits: null,
|
||||||
|
|
||||||
isLoadingUser: false,
|
isLoadingUser: false,
|
||||||
|
|
||||||
|
isRedirectingToBilling: false,
|
||||||
|
|
||||||
|
redirectBillingPortal: async () => {
|
||||||
|
set({ isRedirectingToBilling: true });
|
||||||
|
console.log('redirectBillingPortal');
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(route('api.user.billing_portal'));
|
||||||
|
if (response?.data?.success?.data) {
|
||||||
|
window.location.href = response.data.success.data.redirect;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(route('api.user.billing_portal'));
|
||||||
|
console.error('Error fetching:', error);
|
||||||
|
} finally {
|
||||||
|
set({ isRedirectingToBilling: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Fetch backgrounds
|
// Fetch backgrounds
|
||||||
fetchUser: async () => {
|
fetchUser: async () => {
|
||||||
set({ isLoadingUser: true });
|
set({ isLoadingUser: true });
|
||||||
@@ -27,6 +46,7 @@ const useUserStore = create(
|
|||||||
user_usage: response.data.success.data.user.user_usage,
|
user_usage: response.data.success.data.user.user_usage,
|
||||||
plan: response.data.success.data.user.plan,
|
plan: response.data.success.data.user.plan,
|
||||||
billing: response.data.success.data.billing,
|
billing: response.data.success.data.billing,
|
||||||
|
credits: response.data.success.data.credits,
|
||||||
});
|
});
|
||||||
//return response.data.success.data;
|
//return response.data.success.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
Route::post('/purchase', [UserPurchaseController::class, 'purchase'])->name('api.user.purchase');
|
Route::post('/purchase', [UserPurchaseController::class, 'purchase'])->name('api.user.purchase');
|
||||||
|
|
||||||
|
Route::post('/billing-portal', [UserPurchaseController::class, 'billingPortal'])->name('api.user.billing_portal');
|
||||||
|
|
||||||
Route::post('/logout', [SanctumAuthController::class, 'logout']);
|
Route::post('/logout', [SanctumAuthController::class, 'logout']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,8 +57,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__ . '/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__ . '/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
Route::get('/', [FrontHomeController::class, 'index'])->name('home');
|
Route::get('/', [FrontHomeController::class, 'index'])->name('home');
|
||||||
|
|||||||
Reference in New Issue
Block a user