diff --git a/src/Imprink.WebApi/Controllers/StripeController.cs b/src/Imprink.WebApi/Controllers/StripeController.cs new file mode 100644 index 0000000..aedb2f0 --- /dev/null +++ b/src/Imprink.WebApi/Controllers/StripeController.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Mvc; +using Stripe; + +namespace Imprink.WebApi.Controllers; + +public record CreatePaymentIntentRequest(int Amount, string OrderId); + +[ApiController] +[Route("api/stripe")] +public class StripeController : ControllerBase +{ + [HttpPost("create-payment-intent")] + public async Task CreatePaymentIntent([FromBody] CreatePaymentIntentRequest request) + { + try + { + var options = new PaymentIntentCreateOptions + { + Amount = request.Amount, + Currency = "usd", + PaymentMethodTypes = ["card"], + Metadata = new Dictionary + { + { "order_id", request.OrderId } + } + }; + + var service = new PaymentIntentService(); + var paymentIntent = await service.CreateAsync(options); + + return Ok(new { clientSecret = paymentIntent.ClientSecret }); + } + catch (StripeException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpPost("webhook")] + public async Task HandleWebhook() + { + var json = await new StreamReader(Request.Body).ReadToEndAsync(); + + try + { + var stripeEvent = EventUtility.ConstructEvent( + json, + Request.Headers["Stripe-Signature"], + "whsec_9HyZxZ2HseAkiuRvr4MEP4ntcns9n7FA" + ); + + Console.WriteLine($"Received Stripe event: {stripeEvent.Type}"); + + if (stripeEvent.Type == "payment_intent.succeeded") + { + var paymentIntent = stripeEvent.Data.Object as PaymentIntent; + var orderId = paymentIntent?.Metadata?.GetValueOrDefault("order_id") ?? "Unknown"; + + Console.WriteLine($"✅ Order {orderId} confirmed"); + } + else if (stripeEvent.Type == "payment_intent.payment_failed") + { + var paymentIntent = stripeEvent.Data.Object as PaymentIntent; + var orderId = paymentIntent?.Metadata?.GetValueOrDefault("order_id") ?? "Unknown"; + + Console.WriteLine($"❌ Order {orderId} payment failed"); + } + + return Ok(); + } + catch (StripeException ex) + { + Console.WriteLine($"Webhook error: {ex.Message}"); + return BadRequest(); + } + } + + [HttpGet("health")] + public IActionResult HealthCheck() + { + return Ok(new { status = "OK" }); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs b/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs new file mode 100644 index 0000000..86ee554 --- /dev/null +++ b/src/Imprink.WebApi/Extensions/StartupApplicationExtensions.cs @@ -0,0 +1,36 @@ +using Imprink.Application; +using Imprink.Application.Services; +using Imprink.Domain.Repositories; +using Imprink.Infrastructure; +using Imprink.Infrastructure.Database; +using Imprink.Infrastructure.Repositories; +using Imprink.Infrastructure.Services; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.WebApi.Extensions; + +public static class StartupApplicationExtensions +{ + public static void AddBusinessLogic(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public static void AddDatabaseContexts(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + options.UseSqlServer( + configuration.GetConnectionString("DefaultConnection"), + sqlOptions => sqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Extensions/StartupEnvironmentExtensions.cs b/src/Imprink.WebApi/Extensions/StartupEnvironmentExtensions.cs new file mode 100644 index 0000000..65953bd --- /dev/null +++ b/src/Imprink.WebApi/Extensions/StartupEnvironmentExtensions.cs @@ -0,0 +1,21 @@ +namespace Imprink.WebApi.Extensions; + +public static class StartupEnvironmentExtensions +{ + public static void ConfigureEnvironment(this IApplicationBuilder app, IWebHostEnvironment env) + { + if (app is not WebApplication) return; + + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + app.UseHttpsRedirection(); + } + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Extensions/StartupMigrationExtensions.cs b/src/Imprink.WebApi/Extensions/StartupMigrationExtensions.cs new file mode 100644 index 0000000..d938262 --- /dev/null +++ b/src/Imprink.WebApi/Extensions/StartupMigrationExtensions.cs @@ -0,0 +1,24 @@ +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.WebApi.Extensions; + +public static class StartupMigrationExtensions +{ + public static void ApplyMigrations(this IApplicationBuilder app) + { + if (app is not WebApplication application) return; + + using var scope = application.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + try + { + dbContext.Database.Migrate(); + Console.WriteLine("Database migrations applied successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while applying migrations: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Extensions/StartupSecurityExtensions.cs b/src/Imprink.WebApi/Extensions/StartupSecurityExtensions.cs new file mode 100644 index 0000000..4e4e6f3 --- /dev/null +++ b/src/Imprink.WebApi/Extensions/StartupSecurityExtensions.cs @@ -0,0 +1,46 @@ +using System.Security.Claims; +using Imprink.Infrastructure.Database; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace Imprink.WebApi.Extensions; + +public static class StartupSecurityExtensions +{ + public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.Authority = configuration["Auth0:Authority"]; + options.Audience = configuration["Auth0:Audience"]; + + options.Events = new JwtBearerEvents + { + OnTokenValidated = context => + { + var dbContext = context.HttpContext.RequestServices.GetService(); + var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? context.Principal?.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(userId)) return Task.CompletedTask; + var identity = context.Principal!.Identity as ClaimsIdentity; + + var roles = ( + from ur in dbContext?.UserRole + join r in dbContext?.Roles on ur.RoleId equals r.Id + where ur.UserId == userId + select r.RoleName).ToList(); + + foreach (var role in roles) identity!.AddClaim(new Claim(ClaimTypes.Role, role)); + identity!.AddClaim(new Claim(ClaimTypes.Role, "User")); + + return Task.CompletedTask; + } + }; + }); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Extensions/StartupSwaggerExtensions.cs b/src/Imprink.WebApi/Extensions/StartupSwaggerExtensions.cs new file mode 100644 index 0000000..1f3e6b9 --- /dev/null +++ b/src/Imprink.WebApi/Extensions/StartupSwaggerExtensions.cs @@ -0,0 +1,43 @@ +using Microsoft.OpenApi.Models; + +namespace Imprink.WebApi.Extensions; + +public static class StartupSwaggerExtensions +{ + public static void AddSwaggerWithJwtSecurity(this IServiceCollection services) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Imprink API", + Version = "v1", + }); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme.", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT" + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + [] + } + }); + }); + } +} \ No newline at end of file diff --git a/src/Imprink.WebApi/Imprink.WebApi.csproj b/src/Imprink.WebApi/Imprink.WebApi.csproj index eb637c0..db1976d 100644 --- a/src/Imprink.WebApi/Imprink.WebApi.csproj +++ b/src/Imprink.WebApi/Imprink.WebApi.csproj @@ -18,6 +18,7 @@ + @@ -32,8 +33,4 @@ - - - - diff --git a/src/Imprink.WebApi/Program.cs b/src/Imprink.WebApi/Program.cs index 120a9d8..3695f9a 100644 --- a/src/Imprink.WebApi/Program.cs +++ b/src/Imprink.WebApi/Program.cs @@ -1,5 +1,6 @@ using Imprink.WebApi; using Serilog; +using Stripe; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +11,8 @@ Startup.ConfigureServices(builder); var app = builder.Build(); +StripeConfiguration.ApiKey = "sk_test_51RaxJBRrcXIyofFGsAGYs1umsvqQVmc6stk3R5lumc1qO2Aq6G0EXgCgDeaJ6aHHJ0pyOz4YDglnceKK7eeNUCOx00VBoIIn2z"; + Startup.Configure(app, app.Environment); app.Run(); diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs index 6620633..564050c 100644 --- a/src/Imprink.WebApi/Startup.cs +++ b/src/Imprink.WebApi/Startup.cs @@ -1,19 +1,12 @@ -using System.Security.Claims; using FluentValidation; using Imprink.Application; using Imprink.Application.Commands.Products; -using Imprink.Application.Services; using Imprink.Application.Validation.Users; -using Imprink.Domain.Repositories; -using Imprink.Infrastructure; using Imprink.Infrastructure.Database; -using Imprink.Infrastructure.Repositories; -using Imprink.Infrastructure.Services; +using Imprink.WebApi.Extensions; using Imprink.WebApi.Filters; using Imprink.WebApi.Middleware; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; namespace Imprink.WebApi; @@ -23,142 +16,35 @@ public static class Startup { var services = builder.Services; - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddBusinessLogic(); services.AddAutoMapper(typeof(MappingProfile).Assembly); services.AddHttpContextAccessor(); - services.AddDbContext(options => - options.UseSqlServer( - builder.Configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); + services.AddDatabaseContexts(builder.Configuration); - services.AddMediatR(cfg => - { - cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly); - }); + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly)); services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly); - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.Authority = builder.Configuration["Auth0:Authority"]; - options.Audience = builder.Configuration["Auth0:Audience"]; - - options.Events = new JwtBearerEvents - { - OnTokenValidated = context => - { - var dbContext = context.HttpContext.RequestServices.GetService(); - var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value - ?? context.Principal?.FindFirst("sub")?.Value; - - if (string.IsNullOrEmpty(userId)) return Task.CompletedTask; - var identity = context.Principal!.Identity as ClaimsIdentity; - - var roles = ( - from ur in dbContext?.UserRole - join r in dbContext?.Roles on ur.RoleId equals r.Id - where ur.UserId == userId - select r.RoleName).ToList(); - - foreach (var role in roles) identity!.AddClaim(new Claim(ClaimTypes.Role, role)); - identity!.AddClaim(new Claim(ClaimTypes.Role, "User")); - - return Task.CompletedTask; - } - }; - }); + services.AddJwtAuthentication(builder.Configuration); services.AddAuthorization(); - services.AddControllers(options => - { - options.Filters.Add(); - }); - - services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new OpenApiInfo - { - Title = "Imprink API", - Version = "v1", - }); - - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = "JWT Authorization header using the Bearer scheme.", - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Scheme = "Bearer", - BearerFormat = "JWT" - }); - - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - [] - } - }); - }); + services.AddControllers(options => options.Filters.Add()); + + services.AddSwaggerWithJwtSecurity(); } public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - if (app is WebApplication application) - { - using var scope = application.Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - try - { - dbContext.Database.Migrate(); - Console.WriteLine("Database migrations applied successfully"); - } - catch (Exception ex) - { - Console.WriteLine($"An error occurred while applying migrations: {ex.Message}"); - } - } + app.ApplyMigrations(); app.UseGlobalExceptionHandling(); app.UseRequestTiming(); - if (env.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } - else - { - app.UseExceptionHandler("/Error"); - app.UseHsts(); - app.UseHttpsRedirection(); - } + app.ConfigureEnvironment(env); app.UseRouting(); app.UseAuthentication(); diff --git a/webui/package-lock.json b/webui/package-lock.json index 5742bc1..dfadc04 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -15,6 +15,8 @@ "@mui/material": "^7.1.1", "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^8.5.2", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.3.1", "axios": "^1.9.0", "lucide-react": "^0.516.0", "next": "15.3.3", @@ -1334,6 +1336,29 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.7.0.tgz", + "integrity": "sha512-PYls/2S9l0FF+2n0wHaEJsEU8x7CmBagiH7zYOsxbBlLIHEsqUIQ4MlIAbV9Zg6xwT8jlYdlRIyBTHmO3yM7kQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.3.1.tgz", + "integrity": "sha512-pTzb864TQWDRQBPLgSPFRoyjSDUqpCkbEgTzpsjiTjGz1Z5SxZNXJek28w1s6Dyry4CyW4/Izj5jHE/J9hCJYQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", diff --git a/webui/package.json b/webui/package.json index e5cffc8..98006bd 100644 --- a/webui/package.json +++ b/webui/package.json @@ -16,6 +16,8 @@ "@mui/material": "^7.1.1", "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^8.5.2", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.3.1", "axios": "^1.9.0", "lucide-react": "^0.516.0", "next": "15.3.3", diff --git a/webui/src/app/components/PaymentForm.js b/webui/src/app/components/PaymentForm.js new file mode 100644 index 0000000..3aed71c --- /dev/null +++ b/webui/src/app/components/PaymentForm.js @@ -0,0 +1,103 @@ +'use client'; + +import { useState } from 'react'; +import { + useStripe, + useElements, + PaymentElement, + AddressElement, +} from '@stripe/react-stripe-js'; + +export default function PaymentForm({ onSuccess, orderId }) { + const stripe = useStripe(); + const elements = useElements(); + const [isLoading, setIsLoading] = useState(false); + const [message, setMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!stripe || !elements) { + return; + } + + setIsLoading(true); + setMessage(''); + + const { error } = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: `${window.location.origin}/payment-success`, + }, + redirect: 'if_required', + }); + + if (error) { + if (error.type === 'card_error' || error.type === 'validation_error') { + setMessage(error.message || 'An error occurred'); + } else { + setMessage('An unexpected error occurred.'); + } + } else { + setMessage('Payment successful! 🎉'); + setIsSuccess(true); + setTimeout(() => { + onSuccess(); + }, 2000); + } + + setIsLoading(false); + }; + + const paymentElementOptions = { + layout: 'tabs', + }; + + if (isSuccess) { + return ( +
+
+

✅ Payment Successful!

+

Thank you for your purchase!

+

Order ID: {orderId}

+

You will receive a confirmation email shortly.

+
+
+ ); + } + + return ( +
+
+

Billing Information

+ +
+ +
+

Payment Information

+ +
+ + + + {message && ( +
+ {message} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/webui/src/app/globals.css b/webui/src/app/globals.css new file mode 100644 index 0000000..1638ee2 --- /dev/null +++ b/webui/src/app/globals.css @@ -0,0 +1,246 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', + 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #f8f9fa; + color: #333; + line-height: 1.6; + min-height: 100vh; + padding: 20px 0; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 40px; +} + +header h1 { + color: #333; + margin-bottom: 10px; +} + +header p { + color: #666; + font-size: 16px; +} + +.products h2 { + margin-bottom: 20px; + color: #333; +} + +.product-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.product-card { + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + text-align: center; + transition: transform 0.2s, box-shadow 0.2s; + background: white; +} + +.product-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.product-card h3 { + margin: 0 0 10px 0; + color: #333; +} + +.description { + color: #666; + margin-bottom: 15px; + font-size: 14px; +} + +.price { + font-size: 24px; + font-weight: bold; + color: #0570de; + margin-bottom: 15px; +} + +.select-btn { + background: #0570de; + color: white; + border: none; + padding: 12px 24px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition: background 0.2s; +} + +.select-btn:hover:not(:disabled) { + background: #0458b3; +} + +.select-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.checkout { + max-width: 500px; + margin: 0 auto; +} + +.order-summary { + background: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #ddd; +} + +.order-summary h2 { + margin: 0 0 15px 0; + color: #333; +} + +.order-details p { + margin: 8px 0; + color: #555; +} + +.back-btn { + background: #6c757d; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + margin-top: 20px; + font-size: 14px; +} + +.back-btn:hover { + background: #5a6268; +} + +/* Payment Form Styles */ +.payment-form { + max-width: 500px; + margin: 0 auto; +} + +.payment-section { + margin-bottom: 30px; + background: white; + padding: 20px; + border-radius: 8px; + border: 1px solid #ddd; +} + +.payment-section h3 { + margin: 0 0 15px 0; + color: #333; + font-size: 18px; +} + +.pay-button { + width: 100%; + background: #0570de; + color: white; + border: none; + padding: 16px; + border-radius: 4px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + margin-top: 20px; +} + +.pay-button:hover:not(:disabled) { + background: #0458b3; +} + +.pay-button:disabled { + background: #ccc; + cursor: not-allowed; +} + +.spinner { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.spinner-border { + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.message { + margin-top: 15px; + padding: 12px; + border-radius: 4px; + text-align: center; +} + +.message.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.message.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.success-container { + text-align: center; + padding: 40px 20px; +} + +.success-message { + background: #d4edda; + color: #155724; + padding: 30px; + border: 1px solid #c3e6cb; + border-radius: 8px; +} + +.success-message h2 { + margin: 0 0 15px 0; + color: #155724; +} + +.success-message p { + margin: 10px 0; +} \ No newline at end of file diff --git a/webui/src/app/layout.js b/webui/src/app/layout.js index c649f58..799eaf5 100644 --- a/webui/src/app/layout.js +++ b/webui/src/app/layout.js @@ -1,26 +1,14 @@ -'use client' - -import { CssBaseline } from '@mui/material'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; - -const theme = createTheme({ - palette: { - mode: 'light', - primary: { - main: '#1976d2', - }, - }, -}); +export const metadata = { + title: 'Stripe Payment Demo', + description: 'Stripe payment integration demo with Next.js App Router', +} export default function RootLayout({ children }) { - return ( - - - - - {children} - - - - ); -} + return ( + + + {children} + + + ) +} \ No newline at end of file diff --git a/webui/src/app/page.js b/webui/src/app/page.js index 141ace0..a204683 100644 --- a/webui/src/app/page.js +++ b/webui/src/app/page.js @@ -1,192 +1,143 @@ 'use client'; -import { useUser } from "@auth0/nextjs-auth0"; -import {useEffect, useState} from "react"; +import { useState } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; +import PaymentForm from './components/PaymentForm'; +import './globals.css'; + +const stripePromise = loadStripe('pk_test_51RaxJBRrcXIyofFGYIfUxzWTLPBfr1A0f2VBjo0lOjHfTBtyVpJKBjVUJ972p5AytGl4LBrgQccwHkp6EYu4liln00vEAf2D4e'); + +const products = [ + { id: '1', name: 'Premium Widget', price: 2999, description: 'High-quality widget for professionals' }, + { id: '2', name: 'Standard Widget', price: 1999, description: 'Reliable widget for everyday use' }, + { id: '3', name: 'Basic Widget', price: 999, description: 'Entry-level widget for beginners' } +]; export default function Home() { - const { user, error, isLoading } = useUser(); + const [selectedProduct, setSelectedProduct] = useState(null); + const [clientSecret, setClientSecret] = useState(''); + const [orderId, setOrderId] = useState(''); + const [loading, setLoading] = useState(false); - useEffect(() => { - const fetchAccessToken = async () => { - if (user) { - try { - await fetch('/token'); - } catch (error) { - console.error("Error fetching token"); - } + const handleProductSelect = async (product) => { + setLoading(true); + setSelectedProduct(product); + + const newOrderId = Math.floor(Math.random() * 10000).toString(); + setOrderId(newOrderId); + + try { + const response = await fetch('https://impr.ink/api/stripe/create-payment-intent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + amount: product.price, + orderId: newOrderId + }), + }); + + const data = await response.json(); + + if (data.clientSecret) { + setClientSecret(data.clientSecret); } else { - try { - await fetch('/untoken'); - } catch (e) { - console.error('Error in /api/untoken:', e); - } + console.error('Error creating payment intent:', data.error); } - }; + } catch (error) { + console.error('Error:', error); + } finally { + setLoading(false); + } + }; - fetchAccessToken().then(r => console.log(r)); - }, [user]); + const handlePaymentSuccess = () => { + setSelectedProduct(null); + setClientSecret(''); + setOrderId(''); + }; - if (isLoading) { - return ( -
-
-
-
-
-
-
-
- ); - } + const handleBackToProducts = () => { + setSelectedProduct(null); + setClientSecret(''); + setOrderId(''); + }; - if (error) { - return ( - - ); - } + const appearance = { + theme: 'stripe', + variables: { + colorPrimary: '#0570de', + colorBackground: '#ffffff', + colorText: '#30313d', + colorDanger: '#df1b41', + fontFamily: 'Ideal Sans, system-ui, sans-serif', + spacingUnit: '2px', + borderRadius: '4px', + }, + }; + + const options = { + clientSecret, + appearance, + }; return ( -
-
- {user ? ( -
-
-
- {user.picture ? ( - Profile - ) : ( -
- {user.name?.charAt(0) || user.email?.charAt(0) || '👤'} -
- )} -
-

- Just testing :P -

-
+
+
+

🛍️ Stripe Payment Demo

+

Select a product to purchase

+
-
-
-

- Auth Details -

+ {!selectedProduct ? ( +
+

Products

+
+ {products.map((product) => ( +
+

{product.name}

+

{product.description}

+

${(product.price / 100).toFixed(2)}

+
-
-
-
-
- -
- {user.name || 'Not provided'} -
-
-
- -
- {user.email || 'Not provided'} -
-
-
- -
- {user.sub || 'Not available'} -
-
- {user.nickname && ( -
- -
- {user.nickname} -
-
- )} -
- -
- -
-
-                                                {JSON.stringify(user, null, 2)}
-                                            
-
-
-
-
-
- - +
+ ) : ( +
+
+

Order Summary

+
+

Product: {selectedProduct.name}

+

Order ID: {orderId}

+

Amount: ${(selectedProduct.price / 100).toFixed(2)}

- ) : ( -
- )} -
+ + {clientSecret && ( + + + + )} + + +
+ )}
); } \ No newline at end of file