From 209c022f1d4082d225ad27809644c25782203d4f Mon Sep 17 00:00:00 2001 From: ct Date: Tue, 1 Jul 2025 23:13:09 +0800 Subject: [PATCH] Update --- .../FirstParty/Stripe/StripeHelper.php | 32 +++++++++++ .../Auth/AuthenticatedSessionController.php | 4 +- .../Auth/ConfirmablePasswordController.php | 2 +- ...mailVerificationNotificationController.php | 2 +- .../EmailVerificationPromptController.php | 4 +- .../Auth/RegisteredUserController.php | 4 +- .../Auth/VerifyEmailController.php | 4 +- app/Http/Controllers/SocialAuthController.php | 15 ++++- .../Controllers/UserPurchaseController.php | 29 +++------- app/Http/Middleware/HandleInertiaRequests.php | 7 ++- app/Listeners/StripeEventListener.php | 28 +++++++++ app/Models/Plan.php | 31 ++++++++++ app/Models/User.php | 1 + app/Models/UserPlan.php | 45 +++++++++++++++ .../2025_07_01_145515_create_plans_table.php | 29 ++++++++++ ...5_07_01_145545_create_user_plans_table.php | 32 +++++++++++ database/seeders/PlanSeeder.php | 43 ++++++++++++++ resources/js/components/app-sidebar.tsx | 2 +- resources/js/modules/auth/AuthDialog.jsx | 20 ++++++- .../editor/partials/edit-nav-sidebar.jsx | 57 +++++++++++++++---- resources/js/modules/flash/flash-messages.jsx | 23 ++++++++ resources/js/pages/home/home.tsx | 2 + resources/js/pages/welcome.tsx | 2 +- tests/Feature/Auth/AuthenticationTest.php | 2 +- tests/Feature/Auth/EmailVerificationTest.php | 2 +- tests/Feature/Auth/RegistrationTest.php | 2 +- 26 files changed, 374 insertions(+), 50 deletions(-) create mode 100644 app/Helpers/FirstParty/Stripe/StripeHelper.php create mode 100644 app/Listeners/StripeEventListener.php create mode 100644 app/Models/Plan.php create mode 100644 app/Models/UserPlan.php create mode 100644 database/migrations/2025_07_01_145515_create_plans_table.php create mode 100644 database/migrations/2025_07_01_145545_create_user_plans_table.php create mode 100644 database/seeders/PlanSeeder.php create mode 100644 resources/js/modules/flash/flash-messages.jsx diff --git a/app/Helpers/FirstParty/Stripe/StripeHelper.php b/app/Helpers/FirstParty/Stripe/StripeHelper.php new file mode 100644 index 0000000..8893291 --- /dev/null +++ b/app/Helpers/FirstParty/Stripe/StripeHelper.php @@ -0,0 +1,32 @@ +payload['type']) { + case 'customer.subscription.created': + case 'customer.subscription.updated': + self::handleSubscriptionUpsert($event); + break; + + case 'customer.subscription.deleted': + self::handleSubscriptionDelete($event); + break; + } + } + + private static function handleSubscriptionUpsert(WebhookReceived $event) + { + /// + } + + private static function handleSubscriptionDelete(WebhookReceived $event) + { + /// + } +} diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index b4a48d9..2a7a755 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -33,7 +33,7 @@ public function store(LoginRequest $request): RedirectResponse $request->session()->regenerate(); - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false)); } /** @@ -46,6 +46,6 @@ public function destroy(Request $request): RedirectResponse $request->session()->invalidate(); $request->session()->regenerateToken(); - return redirect('/'); + return redirect(route('home')); } } diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php index c729706..4bf4ef5 100644 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -36,6 +36,6 @@ public function store(Request $request): RedirectResponse $request->session()->put('auth.password_confirmed_at', time()); - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false)); } } diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php index f64fa9b..e6e41c2 100644 --- a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -14,7 +14,7 @@ class EmailVerificationNotificationController extends Controller public function store(Request $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false)); + return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false)); } $request->user()->sendEmailVerificationNotification(); diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php index 672f7cf..f8b61bc 100644 --- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -16,7 +16,7 @@ class EmailVerificationPromptController extends Controller public function __invoke(Request $request): Response|RedirectResponse { return $request->user()->hasVerifiedEmail() - ? redirect()->intended(route('dashboard', absolute: false)) - : Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]); + ? redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false)) + : Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]); } } diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index ea87cde..10f6ef3 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -31,7 +31,7 @@ public function create(): Response public function store(Request $request): RedirectResponse { $request->validate([ - 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, + 'email' => 'required|string|lowercase|email|max:255|unique:' . User::class, 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); @@ -44,6 +44,6 @@ public function store(Request $request): RedirectResponse Auth::login($user); - return to_route('dashboard'); + return to_route(config('platform.general.authed_route_redirect')); } } diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index a300bfa..7674f1a 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -15,7 +15,7 @@ class VerifyEmailController extends Controller public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false) . '?verified=1'); } if ($request->user()->markEmailAsVerified()) { @@ -25,6 +25,6 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse event(new Verified($user)); } - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false) . '?verified=1'); } } diff --git a/app/Http/Controllers/SocialAuthController.php b/app/Http/Controllers/SocialAuthController.php index 9f79d0e..a2325e9 100644 --- a/app/Http/Controllers/SocialAuthController.php +++ b/app/Http/Controllers/SocialAuthController.php @@ -4,6 +4,7 @@ use App; use App\Models\User; +use App\Models\UserPlan; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; @@ -60,7 +61,7 @@ public function handleGoogleCallback() } } - return redirect()->intended(route('home')); + return redirect()->intended(route('home'))->with('success', "You're now logged in!"); } catch (\Exception $e) { throw $e; @@ -73,7 +74,17 @@ public function handleGoogleCallback() } } - private function setupUser($user) {} + private function setupUser($user) + { + $user_plan = UserPlan::where('user_id', $user->id)->first(); + + if (!$user_plan) { + $user_plan = UserPlan::create([ + 'user_id' => $user->id, + 'plan_id' => 'free', + ]); + } + } private function getMockGoogleUser() { diff --git a/app/Http/Controllers/UserPurchaseController.php b/app/Http/Controllers/UserPurchaseController.php index 72e9397..3fa3729 100644 --- a/app/Http/Controllers/UserPurchaseController.php +++ b/app/Http/Controllers/UserPurchaseController.php @@ -32,10 +32,11 @@ public function subscribe(Request $request) $payload = [ 'mode' => 'subscription', - 'success_url' => route('subscribe.success').'?'.'session_id={CHECKOUT_SESSION_ID}', - 'cancel_url' => route('subscribe.cancelled').'?'.'session_id={CHECKOUT_SESSION_ID}', + '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, + 'quantity' => 1, ]], ]; @@ -60,10 +61,7 @@ public function subscribeSuccess(Request $request) 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', - ]); + 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.'); } public function subscribeCancelled(Request $request) @@ -73,10 +71,7 @@ public function subscribeCancelled(Request $request) 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', - ]); + return redirect()->route('home')->with('error', "You've decided not to complete the payment at this time. No charges have been made to your account."); } // PURCHASE (ONE TIME) @@ -86,8 +81,8 @@ 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}', + '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); @@ -112,10 +107,7 @@ public function purchaseSuccess(Request $request) 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', - ]); + return redirect()->route('home')->with('success', "Thank you for purchasing! Your purchase should be active momentarily. Please refresh the page if you do not see your plan."); } public function purchaseCancelled(Request $request) @@ -124,9 +116,6 @@ public function purchaseCancelled(Request $request) 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', - ]); + return redirect()->route('home')->with('error', "You've decided not to complete the payment at this time. No charges have been made to your account."); } } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 04a9d49..ced89c1 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -47,11 +47,16 @@ public function share(Request $request): array 'user' => $request->user(), 'user_is_admin' => user_is_master_admin($request->user()), ], - 'ziggy' => fn (): array => [ + 'ziggy' => fn(): array => [ ...(new Ziggy)->toArray(), 'location' => $request->url(), ], 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', + 'flash' => [ + 'message' => fn() => $request->session()->get('message'), + 'error' => fn() => $request->session()->get('error'), + 'success' => fn() => $request->session()->get('success'), + ], ]; } } diff --git a/app/Listeners/StripeEventListener.php b/app/Listeners/StripeEventListener.php new file mode 100644 index 0000000..553f081 --- /dev/null +++ b/app/Listeners/StripeEventListener.php @@ -0,0 +1,28 @@ + 'int', + 'plan_id' => 'int', + 'current_period_end' => 'datetime', + 'cancel_at' => 'datetime', + 'canceled_at' => 'datetime' + ]; + + protected $fillable = [ + 'user_id', + 'plan_id', + 'current_period_end', + 'cancel_at', + 'canceled_at' + ]; +} diff --git a/database/migrations/2025_07_01_145515_create_plans_table.php b/database/migrations/2025_07_01_145515_create_plans_table.php new file mode 100644 index 0000000..c1a8fbf --- /dev/null +++ b/database/migrations/2025_07_01_145515_create_plans_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('tier'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/database/migrations/2025_07_01_145545_create_user_plans_table.php b/database/migrations/2025_07_01_145545_create_user_plans_table.php new file mode 100644 index 0000000..675ae02 --- /dev/null +++ b/database/migrations/2025_07_01_145545_create_user_plans_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('user_id'); + $table->foreignId('plan_id'); + $table->dateTime('current_period_end')->nullable(); + $table->dateTime('cancel_at')->nullable(); + $table->dateTime('canceled_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_plans'); + } +}; diff --git a/database/seeders/PlanSeeder.php b/database/seeders/PlanSeeder.php new file mode 100644 index 0000000..6bd2b57 --- /dev/null +++ b/database/seeders/PlanSeeder.php @@ -0,0 +1,43 @@ +where('tier', $plan['id']) + ->first(); + + if (! $existingPlan) { + DB::table('plans')->insert([ + 'name' => $plan['name'], + 'tier' => $plan['id'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->command->info("Inserted plan: {$plan['name']}"); + } else { + $this->command->info("Skipped existing plan tier: {$plan['id']}"); + } + } + } +} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 96ece55..334541b 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -10,7 +10,7 @@ import AppLogo from './app-logo'; const mainNavItems: NavItem[] = [ { title: 'Dashboard', - href: route('dashboard'), + href: route(config('platform.general.authed_route_redirect')), icon: LayoutGrid, }, ]; diff --git a/resources/js/modules/auth/AuthDialog.jsx b/resources/js/modules/auth/AuthDialog.jsx index 9cfb80a..74ba2e7 100644 --- a/resources/js/modules/auth/AuthDialog.jsx +++ b/resources/js/modules/auth/AuthDialog.jsx @@ -5,7 +5,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { useMitt } from '@/plugins/MittContext'; import { useEffect, useState } from 'react'; -const AuthDialog = ({ onOpenChange }) => { +const AuthDialog = ({}) => { const emitter = useMitt(); const [isLogin, setIsLogin] = useState(false); @@ -21,13 +21,31 @@ const AuthDialog = ({ onOpenChange }) => { setIsOpen(true); }; + const handleOpenLogin = () => { + setIsLogin(true); + setIsOpen(true); + }; + + const handleOpenJoin = () => { + setIsLogin(false); + setIsOpen(true); + }; + emitter.on('401', handleOpenAuth); + emitter.on('login', handleOpenLogin); + emitter.on('join', handleOpenJoin); return () => { emitter.off('401', handleOpenAuth); + emitter.off('login', handleOpenLogin); + emitter.off('join', handleOpenJoin); }; }, [emitter]); + const onOpenChange = (open) => { + setIsOpen(open); + }; + return ( diff --git a/resources/js/modules/editor/partials/edit-nav-sidebar.jsx b/resources/js/modules/editor/partials/edit-nav-sidebar.jsx index c74459d..550e986 100644 --- a/resources/js/modules/editor/partials/edit-nav-sidebar.jsx +++ b/resources/js/modules/editor/partials/edit-nav-sidebar.jsx @@ -2,16 +2,28 @@ import { usePage } from '@inertiajs/react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { useMitt } from '@/plugins/MittContext'; import useLocalSettingsStore from '@/stores/localSettingsStore'; +import { Link } from '@inertiajs/react'; export default function EditNavSidebar({ isOpen, onClose }) { const { auth } = usePage().props; + const emitter = useMitt(); + const { getSetting, setSetting } = useLocalSettingsStore(); + const openAuth = (isLogin) => { + if (isLogin) { + emitter.emit('login'); + } else { + emitter.emit('join'); + } + }; + return ( !open && onClose()}> - +
MEMEAIGEN
@@ -20,19 +32,42 @@ export default function EditNavSidebar({ isOpen, onClose }) {
- {/* {!auth.user && } - {!auth.user && } */} + {!auth.user && ( + + )} + {!auth.user && ( + + )}
-
- + + {auth.user && ( + + Log out + + )}
diff --git a/resources/js/modules/flash/flash-messages.jsx b/resources/js/modules/flash/flash-messages.jsx new file mode 100644 index 0000000..2bf5c80 --- /dev/null +++ b/resources/js/modules/flash/flash-messages.jsx @@ -0,0 +1,23 @@ +import { usePage } from '@inertiajs/react'; +import { useEffect } from 'react'; +import { toast } from 'sonner'; + +const FlashMessages = () => { + const { flash } = usePage().props; + + useEffect(() => { + if (flash.message) { + toast.success(flash.message); + } + + if (flash.error) { + toast.error(flash.error); + } + + if (flash.success) { + toast.success(flash.success); + } + }, [flash]); +}; + +export default FlashMessages; diff --git a/resources/js/pages/home/home.tsx b/resources/js/pages/home/home.tsx index 99e5c9b..d0cd1d8 100644 --- a/resources/js/pages/home/home.tsx +++ b/resources/js/pages/home/home.tsx @@ -1,9 +1,11 @@ import Editor from '@/modules/editor/editor.jsx'; +import FlashMessages from '@/modules/flash/flash-messages'; const Home = () => { return (
+
); }; diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 2615fdd..6487e7e 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -15,7 +15,7 @@ export default function Welcome() {