MEGA POG STRIPE WORKS

This commit is contained in:
lumijiez
2025-06-21 23:19:16 +03:00
parent d9dfafe07a
commit cae87ee1b7
15 changed files with 779 additions and 325 deletions

View File

@@ -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<IActionResult> CreatePaymentIntent([FromBody] CreatePaymentIntentRequest request)
{
try
{
var options = new PaymentIntentCreateOptions
{
Amount = request.Amount,
Currency = "usd",
PaymentMethodTypes = ["card"],
Metadata = new Dictionary<string, string>
{
{ "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<IActionResult> 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" });
}
}

View File

@@ -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<IProductRepository, ProductRepository>();
services.AddScoped<IProductVariantRepository, ProductVariantRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderItemRepository, OrderItemRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<Seeder>();
}
public static void AddDatabaseContexts(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
sqlOptions => sqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<ApplicationDbContext>();
try
{
dbContext.Database.Migrate();
Console.WriteLine("Database migrations applied successfully");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while applying migrations: {ex.Message}");
}
}
}

View File

@@ -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<ApplicationDbContext>();
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;
}
};
});
}
}

View File

@@ -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"
}
},
[]
}
});
});
}
}

View File

@@ -18,6 +18,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" /> <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Stripe.net" Version="48.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
</ItemGroup> </ItemGroup>
@@ -32,8 +33,4 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,5 +1,6 @@
using Imprink.WebApi; using Imprink.WebApi;
using Serilog; using Serilog;
using Stripe;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -10,6 +11,8 @@ Startup.ConfigureServices(builder);
var app = builder.Build(); var app = builder.Build();
StripeConfiguration.ApiKey = "sk_test_51RaxJBRrcXIyofFGsAGYs1umsvqQVmc6stk3R5lumc1qO2Aq6G0EXgCgDeaJ6aHHJ0pyOz4YDglnceKK7eeNUCOx00VBoIIn2z";
Startup.Configure(app, app.Environment); Startup.Configure(app, app.Environment);
app.Run(); app.Run();

View File

@@ -1,19 +1,12 @@
using System.Security.Claims;
using FluentValidation; using FluentValidation;
using Imprink.Application; using Imprink.Application;
using Imprink.Application.Commands.Products; using Imprink.Application.Commands.Products;
using Imprink.Application.Services;
using Imprink.Application.Validation.Users; using Imprink.Application.Validation.Users;
using Imprink.Domain.Repositories;
using Imprink.Infrastructure;
using Imprink.Infrastructure.Database; using Imprink.Infrastructure.Database;
using Imprink.Infrastructure.Repositories; using Imprink.WebApi.Extensions;
using Imprink.Infrastructure.Services;
using Imprink.WebApi.Filters; using Imprink.WebApi.Filters;
using Imprink.WebApi.Middleware; using Imprink.WebApi.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
namespace Imprink.WebApi; namespace Imprink.WebApi;
@@ -23,142 +16,35 @@ public static class Startup
{ {
var services = builder.Services; var services = builder.Services;
services.AddScoped<IProductRepository, ProductRepository>(); services.AddBusinessLogic();
services.AddScoped<IProductVariantRepository, ProductVariantRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderItemRepository, OrderItemRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<Seeder>();
services.AddAutoMapper(typeof(MappingProfile).Assembly); services.AddAutoMapper(typeof(MappingProfile).Assembly);
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddDbContext<ApplicationDbContext>(options => services.AddDatabaseContexts(builder.Configuration);
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
services.AddMediatR(cfg => services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly));
{
cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly);
});
services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly); services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly);
services.AddAuthentication(options => services.AddJwtAuthentication(builder.Configuration);
{
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<ApplicationDbContext>();
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.AddAuthorization(); services.AddAuthorization();
services.AddControllers(options => services.AddControllers(options => options.Filters.Add<ValidationActionFilter>());
{
options.Filters.Add<ValidationActionFilter>();
});
services.AddSwaggerGen(options => services.AddSwaggerWithJwtSecurity();
{
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"
}
},
[]
}
});
});
} }
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (app is WebApplication application) app.ApplyMigrations();
{
using var scope = application.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
try
{
dbContext.Database.Migrate();
Console.WriteLine("Database migrations applied successfully");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while applying migrations: {ex.Message}");
}
}
app.UseGlobalExceptionHandling(); app.UseGlobalExceptionHandling();
app.UseRequestTiming(); app.UseRequestTiming();
if (env.IsDevelopment()) app.ConfigureEnvironment(env);
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();

View File

@@ -15,6 +15,8 @@
"@mui/material": "^7.1.1", "@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.5.2", "@mui/x-data-grid": "^8.5.2",
"@mui/x-date-pickers": "^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", "axios": "^1.9.0",
"lucide-react": "^0.516.0", "lucide-react": "^0.516.0",
"next": "15.3.3", "next": "15.3.3",
@@ -1334,6 +1336,29 @@
"url": "https://opencollective.com/popperjs" "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": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",

View File

@@ -16,6 +16,8 @@
"@mui/material": "^7.1.1", "@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.5.2", "@mui/x-data-grid": "^8.5.2",
"@mui/x-date-pickers": "^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", "axios": "^1.9.0",
"lucide-react": "^0.516.0", "lucide-react": "^0.516.0",
"next": "15.3.3", "next": "15.3.3",

View File

@@ -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 (
<div className="success-container">
<div className="success-message">
<h2> Payment Successful!</h2>
<p>Thank you for your purchase!</p>
<p><strong>Order ID:</strong> {orderId}</p>
<p>You will receive a confirmation email shortly.</p>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="payment-form">
<div className="payment-section">
<h3>Billing Information</h3>
<AddressElement options={{ mode: 'billing' }} />
</div>
<div className="payment-section">
<h3>Payment Information</h3>
<PaymentElement options={paymentElementOptions} />
</div>
<button
disabled={isLoading || !stripe || !elements}
className="pay-button"
>
{isLoading ? (
<div className="spinner">
<div className="spinner-border"></div>
Processing...
</div>
) : (
'Pay Now'
)}
</button>
{message && (
<div className={`message ${isSuccess ? 'success' : 'error'}`}>
{message}
</div>
)}
</form>
);
}

246
webui/src/app/globals.css Normal file
View File

@@ -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;
}

View File

@@ -1,26 +1,14 @@
'use client' export const metadata = {
title: 'Stripe Payment Demo',
import { CssBaseline } from '@mui/material'; description: 'Stripe payment integration demo with Next.js App Router',
import { ThemeProvider, createTheme } from '@mui/material/styles'; }
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
},
});
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en">
<body className='antialiased'> <body>
<ThemeProvider theme={theme}>
<CssBaseline />
{children} {children}
</ThemeProvider>
</body> </body>
</html> </html>
); )
} }

View File

@@ -1,192 +1,143 @@
'use client'; 'use client';
import { useUser } from "@auth0/nextjs-auth0"; import { useState } from 'react';
import {useEffect, 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() { 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);
const handleProductSelect = async (product) => {
setLoading(true);
setSelectedProduct(product);
const newOrderId = Math.floor(Math.random() * 10000).toString();
setOrderId(newOrderId);
useEffect(() => {
const fetchAccessToken = async () => {
if (user) {
try { try {
await fetch('/token'); const response = await fetch('https://impr.ink/api/stripe/create-payment-intent', {
} catch (error) { method: 'POST',
console.error("Error fetching token"); 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 { } else {
try { console.error('Error creating payment intent:', data.error);
await fetch('/untoken');
} catch (e) {
console.error('Error in /api/untoken:', e);
} }
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
} }
}; };
fetchAccessToken().then(r => console.log(r)); const handlePaymentSuccess = () => {
}, [user]); setSelectedProduct(null);
setClientSecret('');
setOrderId('');
};
if (isLoading) { const handleBackToProducts = () => {
return ( setSelectedProduct(null);
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 flex items-center justify-center"> setClientSecret('');
<div className="relative"> setOrderId('');
<div className="w-16 h-16 border-4 border-white/20 border-t-white rounded-full animate-spin"></div> };
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="w-10 h-10 border-4 border-transparent border-t-purple-400 rounded-full animate-spin"></div> const appearance = {
</div> theme: 'stripe',
</div> variables: {
</div> colorPrimary: '#0570de',
); colorBackground: '#ffffff',
} colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: 'Ideal Sans, system-ui, sans-serif',
spacingUnit: '2px',
borderRadius: '4px',
},
};
const options = {
clientSecret,
appearance,
};
if (error) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-red-900 via-pink-900 to-purple-900 flex items-center justify-center p-4"> <div className="container">
<div className="bg-white/10 backdrop-blur-xl rounded-2xl p-6 border border-white/20 shadow-2xl"> <header>
<div className="text-white/80 mb-4">{error.message}</div> <h1>🛍 Stripe Payment Demo</h1>
<div className="text-center"> <p>Select a product to purchase</p>
<a </header>
href="/auth/login"
className="group relative inline-flex items-center gap-2 px-8 py-3 bg-gradient-to-r from-purple-500 to-blue-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-purple-500/25 transition-all duration-300 hover:scale-105 active:scale-95" {!selectedProduct ? (
<div className="products">
<h2>Products</h2>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p className="description">{product.description}</p>
<p className="price">${(product.price / 100).toFixed(2)}</p>
<button
onClick={() => handleProductSelect(product)}
disabled={loading}
className="select-btn"
> >
<div {loading ? 'Loading...' : 'Select'}
className="absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </button>
<span className="relative flex items-center gap-2"> </div>
Sign In ))}
</span>
</a>
<a
onClick={() => checkValidity()}
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
>
<div
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span className="relative flex items-center gap-2">
Check
</span>
</a>
</div> </div>
</div> </div>
) : (
<div className="checkout">
<div className="order-summary">
<h2>Order Summary</h2>
<div className="order-details">
<p><strong>Product:</strong> {selectedProduct.name}</p>
<p><strong>Order ID:</strong> {orderId}</p>
<p><strong>Amount:</strong> ${(selectedProduct.price / 100).toFixed(2)}</p>
</div>
</div> </div>
);
}
return ( {clientSecret && (
<div <Elements options={options} stripe={stripePromise}>
className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 relative overflow-hidden"> <PaymentForm
<div className="relative z-10 min-h-screen flex items-center justify-center p-4"> onSuccess={handlePaymentSuccess}
{user ? ( orderId={orderId}
<div className="w-full max-w-5xl">
<div className="text-center mb-6">
<div
className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full mb-3 shadow-2xl">
{user.picture ? (
<img
src={user.picture}
alt="Profile"
className="w-full h-full rounded-full object-cover border-3 border-white/20"
/> />
) : ( </Elements>
<div className="text-white text-xl font-bold">
{user.name?.charAt(0) || user.email?.charAt(0) || '👤'}
</div>
)} )}
</div>
<h1 className="text-2xl pb-1 font-bold bg-gradient-to-r from-white via-purple-200 to-blue-200 bg-clip-text text-transparent">
Just testing :P
</h1>
</div>
<div className="bg-white/10 backdrop-blur-xl rounded-2xl border border-white/20 shadow-2xl overflow-hidden mb-4"> <button
<div className="bg-gradient-to-r from-purple-500/20 to-blue-500/20 p-4 border-b border-white/10"> onClick={handleBackToProducts}
<h2 className="text-xl font-bold text-white flex items-center gap-2"> className="back-btn"
Auth Details
</h2>
</div>
<div className="p-5">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Name</label>
<div
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
{user.name || 'Not provided'}
</div>
</div>
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Email</label>
<div
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
{user.email || 'Not provided'}
</div>
</div>
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">User
ID</label>
<div
className="text-white/80 text-xs mt-1 p-2 bg-white/5 rounded-lg border border-white/10 font-mono break-all">
{user.sub || 'Not available'}
</div>
</div>
{user.nickname && (
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Nickname</label>
<div
className="text-white text-base mt-1 p-2 bg-white/5 rounded-lg border border-white/10">
{user.nickname}
</div>
</div>
)}
</div>
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider mb-2 block">
Raw User Data
</label>
<div
className="bg-black/30 rounded-lg p-3 border border-white/10 h-64 overflow-auto">
<pre
className="text-green-300 text-xs font-mono leading-tight whitespace-pre-wrap">
{JSON.stringify(user, null, 2)}
</pre>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-center">
<a
onClick={() => checkValidity()}
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
> >
<div Back to Products
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </button>
<span className="relative flex items-center gap-2">
Check
</span>
</a>
<a
href="/auth/logout"
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
>
<div
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span className="relative flex items-center gap-2">
Sign Out
</span>
</a>
</div> </div>
</div>
) : (
<div></div>
)} )}
</div> </div>
</div>
); );
} }