MEGA POG STRIPE WORKS
This commit is contained in:
83
src/Imprink.WebApi/Controllers/StripeController.cs
Normal file
83
src/Imprink.WebApi/Controllers/StripeController.cs
Normal 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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Imprink.WebApi/Extensions/StartupMigrationExtensions.cs
Normal file
24
src/Imprink.WebApi/Extensions/StartupMigrationExtensions.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Imprink.WebApi/Extensions/StartupSecurityExtensions.cs
Normal file
46
src/Imprink.WebApi/Extensions/StartupSecurityExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Imprink.WebApi/Extensions/StartupSwaggerExtensions.cs
Normal file
43
src/Imprink.WebApi/Extensions/StartupSwaggerExtensions.cs
Normal 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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
25
webui/package-lock.json
generated
25
webui/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
103
webui/src/app/components/PaymentForm.js
Normal file
103
webui/src/app/components/PaymentForm.js
Normal 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
246
webui/src/app/globals.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user