diff --git a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php index d41e3d1..d2ccf19 100644 --- a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php +++ b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php @@ -89,26 +89,33 @@ private static function handleSubscriptionDelete(WebhookReceived $event) if ($user) { - $plan = Plan::where('tier', 'free')->first(); + foreach ($object['items']['data'] as $line_item) { - if ($plan) { - $user_plan = UserPlan::where('user_id', $user->id)->first(); + $stripe_price_id = $line_item['plan']['id']; - 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, - ]); + $subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id); + + if ($subscription_config['type'] == 'subscription_plans') { + $free_plan = Plan::where('tier', 'free')->first(); + + $user_plan = UserPlan::where('user_id', $user->id)->first(); + + if ($user_plan) { + $user_plan->update([ + 'plan_id' => $free_plan->id, + 'current_period_end' => null, + 'cancel_at' => null, + 'canceled_at' => null, + ]); + } else { + $user_plan = UserPlan::create([ + 'user_id' => $user->id, + 'plan_id' => $free_plan->id, + 'current_period_end' => null, + 'cancel_at' => null, + 'canceled_at' => null, + ]); + } } } } diff --git a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php index ae1d5ec..ed79b00 100644 --- a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php +++ b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php @@ -14,6 +14,44 @@ public static function handleWatermarkUsageWebhookEvents(WebhookReceived $event) case 'invoice.paid': self::handleInvoicePaid($event); break; + + case 'customer.subscription.deleted': + self::handleClearWatermarkCredits($event); + break; + } + } + + private static function handleClearWatermarkCredits(WebhookReceived $event) + { + $object = StripeHelper::getEventObject($event); + + $user = StripeHelper::getUserByStripeID($object['customer']); + + if ($user) { + + foreach ($object['items']['data'] as $line_item) { + + $stripe_price_id = $line_item['plan']['id']; + + $subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id); + + if ($subscription_config['type'] == 'subscription_plans') { + // revert the watermark credits back to 0 + + $user_usage = UserUsage::where('user_id', $user->id)->first(); + + if ($user_usage) { + $user_usage->update([ + 'non_watermark_videos_left' => 0, + ]); + } else { + $user_usage = UserUsage::create([ + 'user_id' => $user->id, + 'non_watermark_videos_left' => 0, + ]); + } + } + } } } diff --git a/app/Http/Controllers/UserExportController.php b/app/Http/Controllers/UserExportController.php new file mode 100644 index 0000000..128d96e --- /dev/null +++ b/app/Http/Controllers/UserExportController.php @@ -0,0 +1,62 @@ +load('user_usage'); + + if ($user->user_usage->non_watermark_videos_left <= 0) { + return response()->json([ + 'error' => [ + 'message' => 'You have no credits left to export.', + ], + ]); + } + + return response()->json([ + 'success' => [ + 'data' => [ + 'user_usage' => $user->user_usage, + ], + ], + ]); + } + + public function premiumExportComplete(Request $request) + { + $user = Auth::user(); + + $user->load('user_usage'); + + if ($user->user_usage->non_watermark_videos_left <= 0) { + return response()->json([ + 'error' => [ + 'message' => 'You have no credits left to export.', + ], + ]); + } + + $user->user_usage->update([ + 'non_watermark_videos_left' => $user->user_usage->non_watermark_videos_left - 1, + ]); + + $user->user_usage->refresh(); + + return response()->json([ + 'success' => [ + 'data' => [ + 'user_usage' => $user->user_usage, + ], + ], + ]); + } +} diff --git a/resources/js/modules/editor/partials/canvas/video-download/video-download-modal.jsx b/resources/js/modules/editor/partials/canvas/video-download/video-download-modal.jsx index 2668a1b..edc6aa1 100644 --- a/resources/js/modules/editor/partials/canvas/video-download/video-download-modal.jsx +++ b/resources/js/modules/editor/partials/canvas/video-download/video-download-modal.jsx @@ -4,6 +4,7 @@ import { Progress } from '@/components/ui/progress'; import { Textarea } from '@/components/ui/textarea'; import { Clock10Icon, Download, Droplets } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; +import useUserStore from '@/stores/UserStore'; const VideoDownloadModal = ({ nonWatermarkVideosLeft = 0, @@ -16,32 +17,47 @@ const VideoDownloadModal = ({ exportStatus, }) => { const [showDebug, setShowDebug] = useState(false); - const [exportType, setExportType] = useState(null); + const [isPremiumExport, setIsPremiumExport] = useState(false); const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null); const [status, setStatus] = useState('start'); // 'start', 'processing', 'complete' const exportStartTime = useRef(null); const lastProgressTime = useRef(null); const lastProgress = useRef(0); + + const { premiumExportRequest, premiumExportComplete } = useUserStore(); - const handleExportWithoutWatermark = () => { - setExportType('without'); + const handleExportWithoutWatermark = async () => { + setIsPremiumExport(true); setEstimatedTimeRemaining(null); - setStatus('processing'); - handleDownloadButton(); + + // Call premiumExportRequest and check response + const response = await premiumExportRequest(); + + if (response?.error) { + // Halt the process if there's an error + setIsPremiumExport(false); + return; + } + + if (response?.success) { + // Continue with export if successful + setStatus('processing'); + handleDownloadButton(); + } }; const handleExportWithWatermark = () => { - setExportType('with'); + setIsPremiumExport(false); setEstimatedTimeRemaining(null); setStatus('processing'); handleDownloadButton(); }; - const handleClose = () => { + const handleClose = async () => { onClose(); setTimeout(() => { - setExportType(null); + setIsPremiumExport(false); setEstimatedTimeRemaining(null); setStatus('start'); exportStartTime.current = null; @@ -54,8 +70,12 @@ const VideoDownloadModal = ({ useEffect(() => { if (status === 'processing' && exportProgress >= 100) { setStatus('complete'); + // Call premiumExportComplete immediately when export completes + if (isPremiumExport) { + premiumExportComplete(); + } } - }, [exportProgress, status]); + }, [exportProgress, status, isPremiumExport, premiumExportComplete]); // Calculate estimated time remaining based on progress speed useEffect(() => { @@ -183,7 +203,7 @@ const VideoDownloadModal = ({ className="h-14 w-full text-base font-medium shadow-md transition-all duration-200 hover:shadow-lg" size="lg" > - Export without watermark ({nonWatermarkVideosLeft} left) + Premium Export, no watermark ({nonWatermarkVideosLeft} left) )} diff --git a/resources/js/modules/upgrade/upgrade-sheet.jsx b/resources/js/modules/upgrade/upgrade-sheet.jsx index 02c4340..9118687 100644 --- a/resources/js/modules/upgrade/upgrade-sheet.jsx +++ b/resources/js/modules/upgrade/upgrade-sheet.jsx @@ -122,7 +122,7 @@ const UpgradeSheet = () => {
{/* Non-watermark Exports */} - {user_usage?.non_watermark_videos_left && ( + {user_usage?.non_watermark_videos_left != null && (
diff --git a/resources/js/stores/UserStore.js b/resources/js/stores/UserStore.js index 471104e..c48797f 100644 --- a/resources/js/stores/UserStore.js +++ b/resources/js/stores/UserStore.js @@ -1,5 +1,6 @@ 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'; @@ -56,6 +57,47 @@ const useUserStore = create( set({ isLoadingUser: false }); } }, + + premiumExportRequest: async () => { + try { + const response = await axiosInstance.post(route('api.user.premium_export.request')); + + if (response?.data?.success?.data?.user_usage) { + set({ + user_usage: response.data.success.data.user_usage, + }); + } + + if (response?.data?.error?.message) { + toast.error(response.data.error.message); + } + return response.data; + } catch (error) { + console.error(route('api.user.premium_export.request')); + console.error('Error fetching:', error); + } + }, + + premiumExportComplete: async () => { + try { + const response = await axiosInstance.post(route('api.user.premium_export.complete')); + + if (response?.data?.success?.data?.user_usage) { + set({ + user_usage: response.data.success.data.user_usage, + }); + } + + if (response?.data?.error?.message) { + toast.error(response.data.error.message); + } + + return response.data; + } catch (error) { + console.error(route('api.user.premium_export.complete')); + console.error('Error fetching:', error); + } + }, })), { name: 'UserStore', diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js index 7df5e16..4a32145 100644 --- a/resources/js/ziggy.js +++ b/resources/js/ziggy.js @@ -1,4 +1,4 @@ -const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"cashier.payment":{"uri":"stripe\/payment\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"cashier.webhook":{"uri":"stripe\/webhook","methods":["POST"]},"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.pricing_page":{"uri":"api\/pricing","methods":["POST"]},"api.user":{"uri":"api\/user","methods":["POST"]},"api.user.subscribe":{"uri":"api\/user\/subscribe","methods":["POST"]},"api.user.purchase":{"uri":"api\/user\/purchase","methods":["POST"]},"api.user.billing_portal":{"uri":"api\/user\/billing-portal","methods":["POST"]},"api.app.init":{"uri":"api\/app\/init","methods":["POST"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"auth.google.redirect":{"uri":"auth\/google\/redirect","methods":["GET","HEAD"]},"auth.google.callback":{"uri":"auth\/google\/callback","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"subscribe.success":{"uri":"subscribe\/success","methods":["GET","HEAD"]},"subscribe.cancelled":{"uri":"subscribe\/cancelled","methods":["GET","HEAD"]},"purchase.success":{"uri":"purchase\/success","methods":["GET","HEAD"]},"purchase.cancelled":{"uri":"purchase\/cancelled","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"admin.background-generation":{"uri":"admin\/background-generation","methods":["GET","HEAD"]},"admin.background-generation.generate":{"uri":"admin\/background-generation\/generate","methods":["POST"]},"admin.background-generation.save":{"uri":"admin\/background-generation\/save","methods":["POST"]},"admin.background-generation.delete":{"uri":"admin\/background-generation\/delete\/{id}","methods":["POST"],"parameters":["id"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; +const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"cashier.payment":{"uri":"stripe\/payment\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"cashier.webhook":{"uri":"stripe\/webhook","methods":["POST"]},"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"api.pricing_page":{"uri":"api\/pricing","methods":["POST"]},"api.user":{"uri":"api\/user","methods":["POST"]},"api.user.subscribe":{"uri":"api\/user\/subscribe","methods":["POST"]},"api.user.purchase":{"uri":"api\/user\/purchase","methods":["POST"]},"api.user.billing_portal":{"uri":"api\/user\/billing-portal","methods":["POST"]},"api.user.premium_export.request":{"uri":"api\/user\/premium-export\/request","methods":["POST"]},"api.user.premium_export.complete":{"uri":"api\/user\/premium-export\/complete","methods":["POST"]},"api.app.init":{"uri":"api\/app\/init","methods":["POST"]},"api.app.memes":{"uri":"api\/app\/memes","methods":["POST"]},"api.app.background":{"uri":"api\/app\/background","methods":["POST"]},"auth.google.redirect":{"uri":"auth\/google\/redirect","methods":["GET","HEAD"]},"auth.google.callback":{"uri":"auth\/google\/callback","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"subscribe.success":{"uri":"subscribe\/success","methods":["GET","HEAD"]},"subscribe.cancelled":{"uri":"subscribe\/cancelled","methods":["GET","HEAD"]},"purchase.success":{"uri":"purchase\/success","methods":["GET","HEAD"]},"purchase.cancelled":{"uri":"purchase\/cancelled","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"admin.background-generation":{"uri":"admin\/background-generation","methods":["GET","HEAD"]},"admin.background-generation.generate":{"uri":"admin\/background-generation\/generate","methods":["POST"]},"admin.background-generation.save":{"uri":"admin\/background-generation\/save","methods":["POST"]},"admin.background-generation.delete":{"uri":"admin\/background-generation\/delete\/{id}","methods":["POST"],"parameters":["id"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { Object.assign(Ziggy.routes, window.Ziggy.routes); } diff --git a/routes/api.php b/routes/api.php index 3164feb..ae2457e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Auth\SanctumAuthController; use App\Http\Controllers\FrontMediaController; use App\Http\Controllers\UserAccountController; +use App\Http\Controllers\UserExportController; use App\Http\Controllers\UserPurchaseController; use Illuminate\Support\Facades\Route; @@ -26,6 +27,10 @@ Route::post('/billing-portal', [UserPurchaseController::class, 'billingPortal'])->name('api.user.billing_portal'); Route::post('/logout', [SanctumAuthController::class, 'logout']); + + Route::post('/premium-export/request', [UserExportController::class, 'premiumExportRequest'])->name('api.user.premium_export.request'); + + Route::post('/premium-export/complete', [UserExportController::class, 'premiumExportComplete'])->name('api.user.premium_export.complete'); }); });