diff --git a/app/Http/Controllers/StripeController.php b/app/Http/Controllers/StripeController.php new file mode 100644 index 0000000..83ac81a --- /dev/null +++ b/app/Http/Controllers/StripeController.php @@ -0,0 +1,153 @@ +cardRepository = $cardRepository; + } + + + public function createCheckoutSession(Request $request) + { + + Stripe::setApiKey(env('STRIPE_SECRET_KEY')); + + $count = $request->input('count'); + $clientSessionId = Str::uuid(); + + $priceIds = [ + 3 => 'price_1S51zxGaZ3yeYkzWYb0wSt4j', + 4 => 'price_1S5464GaZ3yeYkzWh8RuJfab', + ]; + + if (!isset($priceIds[$count])) { + return response()->json(['error' => 'Invalid product selected.'], 400); + } + + try { + $session = Session::create([ + 'line_items' => [[ + 'price' => $priceIds[$count], + 'quantity' => 1, + ]], + 'mode' => 'payment', + 'success_url' => url(env('APP_URL') . '/success?client_session_id=' . $clientSessionId), + 'cancel_url' => url(env('APP_URL') . '/cancel'), + 'metadata' => [ + 'draw_count' => $request->input('count'), + 'client_session_id' => $clientSessionId, + ], + ]); + + Payment::create([ + 'amount' => $session->amount_total / 100, + 'currency' => $session->currency, + 'stripe_session_id' => $session->id, + 'client_session_id' => $clientSessionId, + 'draw_count' => $count, + 'status' => 'pending', + ]); + + return response()->json(['sessionId' => $session->id]); + + } catch (\Exception $e) { + \Log::error('Stripe session creation failed: ' . $e->getMessage()); + return response()->json(['error' => 'Could not create checkout session.'], 500); + } + } + + public function validatePayment(Request $request) + { + $clientSessionId = $request->query('client_session_id'); + + $payment = Payment::where('client_session_id', $clientSessionId) + ->where('status', 'succeeded') + ->first(); + + if ($payment) { + // Si la vérification réussit, retournez le nombre de tirages. + return response()->json([ + 'success' => true, + 'drawCount' => $payment->draw_count, + ]); + } + + // Si la vérification échoue, retournez une erreur. + return response()->json([ + 'success' => false, + 'message' => 'Paiement non validé.', + ], 404); + } + + public function getCards(Request $request) + { + $sessionId = $request->query('client_session_id'); + + if(!$sessionId) + { + $count = $request->query('count'); + if($count == 1){ + $freeCards = $this->cardRepository->draw(1); + return response()->json([ + 'success' => true, + 'cards' => $freeCards + ]); + } + } + + // 1. Find the payment record + $payment = Payment::where('client_session_id', $sessionId)->first(); + + if (!$payment) { + return response()->json(['success' => false, 'message' => 'Payment not found.'], 404); + } + + // 2. One-Time Use Check + if ($payment->status === 'processed') { + return response()->json([ + 'success' => true, + 'cards' => $payment->cards, + 'message' => 'Cards already drawn for this payment.', + ]); + } + + // 3. Verify payment status with Stripe + if ($payment->status !== 'succeeded') { + try { + $session = Session::retrieve($sessionId); + if ($session->payment_status !== 'paid' || $session->status !== 'complete') { + return response()->json(['success' => false, 'message' => 'Payment not complete.'], 402); + } + $payment->update(['status' => 'succeeded']); + } catch (\Exception $e) { + \Log::error('Stripe session retrieval failed: ' . $e->getMessage()); + return response()->json(['success' => false, 'message' => 'Validation error.'], 500); + } + } + + // 4. Securely draw the cards and store them + $drawnCards = $this->cardRepository->draw($payment->draw_count); + $payment->update([ + 'cards' => $drawnCards, + 'status' => 'processed', + ]); + return response()->json([ + 'success' => true, + 'cards' => $drawnCards, + ]); + } + +} diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php new file mode 100644 index 0000000..7a02520 --- /dev/null +++ b/app/Http/Controllers/WebhookController.php @@ -0,0 +1,62 @@ +getContent(); + $signature = $request->header('Stripe-Signature'); + $endpointSecret = env('STRIPE_WEBHOOK_SECRET'); + + try { + $event = Webhook::constructEvent($payload, $signature, $endpointSecret); + } catch (SignatureVerificationException $e) { + Log::error('Stripe webhook signature verification failed: ' . $e->getMessage()); + return response()->json(['error' => 'Invalid signature'], 400); + } + + try { + // Handle the event + switch ($event->type) { + case 'checkout.session.completed': + $session = $event->data->object; + $drawCount = $session->metadata->draw_count; + $clientSessionId = $session->metadata->client_session_id; + + $payment = Payment::where('client_session_id', $clientSessionId)->first(); + + if ($payment) { + $payment->update([ + 'status' => 'succeeded', + 'draw_count' => $drawCount, + ]); + } else { + // Log if no matching payment record is found + Log::warning('No pending payment record found for client_session_id: ' . $clientSessionId); + } + + break; + default: + Log::info('Received a non-checkout.session.completed webhook event: ' . $event->type); + break; + } + } catch (\Exception $e) { + // Log any other unexpected errors + Log::error('Stripe webhook processing error: ' . $e->getMessage(), ['exception' => $e]); + return response()->json(['error' => 'Server error'], 500); + } + + return response()->json(['status' => 'success'], 200); + } +} diff --git a/app/Models/Payment.php b/app/Models/Payment.php new file mode 100644 index 0000000..bd355a2 --- /dev/null +++ b/app/Models/Payment.php @@ -0,0 +1,37 @@ + 'decimal:2', + 'cards' => 'array', + ]; + +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 134581a..09857a3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -10,6 +10,7 @@ use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) @@ -21,6 +22,10 @@ return Application::configure(basePath: dirname(__DIR__)) HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, ]); + + $middleware->validateCsrfTokens(except: [ + 'stripe/*', + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/composer.json b/composer.json index 8f9af7e..11e9eb6 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,10 @@ "php": "^8.2", "inertiajs/inertia-laravel": "^2.0", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1", - "laravel/wayfinder": "^0.1.9" + "laravel/wayfinder": "^0.1.9", + "stripe/stripe-php": "^17.6" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -83,4 +85,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 5c1791e..a89534e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "644df20f057e10d119c33b325af1c6cb", + "content-hash": "7a72790164b9b6dc081f7cbfde7e67d5", "packages": [ { "name": "brick/math", @@ -1399,6 +1399,70 @@ }, "time": "2025-07-07T14:17:42+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-07-09T19:45:24+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.4", @@ -3406,6 +3470,65 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v17.6.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/a6219df5df1324a0d3f1da25fb5e4b8a3307ea16", + "reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.72.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/v17.6.0" + }, + "time": "2025-08-27T19:32:42+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2025_09_08_132642_add_columns_draw_count_user.php b/database/migrations/2025_09_08_132642_add_columns_draw_count_user.php new file mode 100644 index 0000000..2929dfe --- /dev/null +++ b/database/migrations/2025_09_08_132642_add_columns_draw_count_user.php @@ -0,0 +1,26 @@ +integer('paid_draws')->default(1); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $table->dropColumn('paid_draws'); + } +}; diff --git a/database/migrations/2025_09_08_134207_add_draw_count_and_client_session_id_to_payments_table.php b/database/migrations/2025_09_08_134207_add_draw_count_and_client_session_id_to_payments_table.php new file mode 100644 index 0000000..85c2414 --- /dev/null +++ b/database/migrations/2025_09_08_134207_add_draw_count_and_client_session_id_to_payments_table.php @@ -0,0 +1,29 @@ +integer('draw_count')->nullable()->after('status'); + $table->string('client_session_id', 255)->nullable()->after('stripe_session_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('payments', function (Blueprint $table) { + $table->dropColumn(['draw_count', 'client_session_id']); + }); + } +}; diff --git a/database/migrations/2025_09_08_170714_create_personal_access_tokens_table.php b/database/migrations/2025_09_08_170714_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2025_09_08_170714_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2025_09_08_194556_edit_table_payments.php b/database/migrations/2025_09_08_194556_edit_table_payments.php new file mode 100644 index 0000000..0f43a28 --- /dev/null +++ b/database/migrations/2025_09_08_194556_edit_table_payments.php @@ -0,0 +1,36 @@ +enum('status', ['pending', 'succeeded', 'failed', 'refunded', 'processed'])->default('pending')->change(); + + // Add the cards JSON column + $table->json('cards')->nullable()->after('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('payments', function (Blueprint $table) { + // Revert the status enum to its original values + $table->enum('status', ['pending', 'succeeded', 'failed', 'refunded'])->default('pending')->change(); + + // Remove the cards column + $table->dropColumn('cards'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index e09225b..dab4a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@inertiajs/vue3": "^2.1.0", + "@stripe/stripe-js": "^7.9.0", "@vue-stripe/vue-stripe": "^4.5.0", "@vueuse/core": "^12.8.2", "axios": "^1.11.0", @@ -14,11 +15,15 @@ "laravel-vite-plugin": "^2.0.0", "lucide-vue-next": "^0.468.0", "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.5.0", "reka-ui": "^2.2.0", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.1", "tw-animate-css": "^1.2.5", - "vue": "^3.5.13" + "uuid": "^12.0.0", + "vue": "^3.5.13", + "vue-router": "^4.5.1", + "vue-stripe-js": "^2.0.2" }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -1227,9 +1232,12 @@ ] }, "node_modules/@stripe/stripe-js": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", - "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==" + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", + "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", + "engines": { + "node": ">=12.16" + } }, "node_modules/@swc/helpers": { "version": "0.5.17", @@ -1871,6 +1879,11 @@ "vue-coerce-props": "^1.0.0" } }, + "node_modules/@vue-stripe/vue-stripe/node_modules/@stripe/stripe-js": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", + "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==" + }, "node_modules/@vue/compiler-core": { "version": "3.5.18", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", @@ -2512,6 +2525,11 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-pick-omit": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz", + "integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==" + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -2527,6 +2545,11 @@ "node": ">=0.4.0" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4155,6 +4178,32 @@ } } }, + "node_modules/pinia-plugin-persistedstate": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz", + "integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==", + "dependencies": { + "deep-pick-omit": "^1.2.1", + "defu": "^6.1.4", + "destr": "^2.0.5" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "@pinia/nuxt": ">=0.10.0", + "pinia": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@pinia/nuxt": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4956,6 +5005,18 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-12.0.0.tgz", + "integrity": "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", @@ -5139,6 +5200,41 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/vue-stripe-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vue-stripe-js/-/vue-stripe-js-2.0.2.tgz", + "integrity": "sha512-tYDUw0zzXfo7kTTyTAFYCDZP7BPq1TM5mAgCRcjcLg7IgUiJlSBcnReDoCCZ6RDhSjFC4Yl5hwKO5Zs38fV63A==", + "dependencies": { + "@stripe/stripe-js": "^5.5.0" + } + }, + "node_modules/vue-stripe-js/node_modules/@stripe/stripe-js": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz", + "integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==", + "engines": { + "node": ">=12.16" + } + }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", diff --git a/package.json b/package.json index 07b5344..70ff2ad 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@inertiajs/vue3": "^2.1.0", + "@stripe/stripe-js": "^7.9.0", "@vue-stripe/vue-stripe": "^4.5.0", "@vueuse/core": "^12.8.2", "axios": "^1.11.0", @@ -38,11 +39,15 @@ "laravel-vite-plugin": "^2.0.0", "lucide-vue-next": "^0.468.0", "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.5.0", "reka-ui": "^2.2.0", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.1", "tw-animate-css": "^1.2.5", - "vue": "^3.5.13" + "uuid": "^12.0.0", + "vue": "^3.5.13", + "vue-router": "^4.5.1", + "vue-stripe-js": "^2.0.2" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", diff --git a/resources/js/app.ts b/resources/js/app.ts index 3269fc5..71d8d1e 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -3,12 +3,14 @@ import '../css/app.css'; import { createInertiaApp } from '@inertiajs/vue3'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import type { DefineComponent } from 'vue'; import { createApp, h } from 'vue'; import { initializeTheme } from './composables/useAppearance'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; const pinia = createPinia(); +pinia.use(piniaPluginPersistedstate); createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), diff --git a/resources/js/components/landing/HeroSection.vue b/resources/js/components/landing/HeroSection.vue index 476a669..b61f127 100644 --- a/resources/js/components/landing/HeroSection.vue +++ b/resources/js/components/landing/HeroSection.vue @@ -1,3 +1,11 @@ + +