diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs index fbdabd4ce..19af46bbd 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Abstractions/IStripePaymentService.cs @@ -17,6 +17,8 @@ namespace OrchardCore.Commerce.Payment.Stripe.Abstractions; /// public interface IStripePaymentService { + long GetPaymentAmount(Amount total); + /// /// Returns the public key of the Stripe account. /// diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs index 51431ae43..5268134f8 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Api/StripeEndpoint.cs @@ -4,19 +4,51 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using OrchardCore.Commerce.Endpoints; using OrchardCore.Commerce.Endpoints.Permissions; +using OrchardCore.Commerce.MoneyDataType; +using OrchardCore.Commerce.Payment.Abstractions; using OrchardCore.Commerce.Payment.Stripe.Abstractions; +using OrchardCore.Commerce.Payment.Stripe.EndPoints.Models; +using OrchardCore.Commerce.Payment.Stripe.Models; +using OrchardCore.Commerce.Payment.Stripe.Services; using OrchardCore.Modules; -using Stripe; +using OrchardCore.Settings; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace OrchardCore.Commerce.Payment.Stripe.EndPoints.Api; public static class StripeEndpoint { + // Get total amount + public static IEndpointRouteBuilder AddStripeTotalEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapGet("api/checkout/stripe/total/{shoppingCartId?}", GetStripeTotalAsync) + .AllowAnonymous() + .DisableAntiforgery(); + + return builder; + } + + private static async Task GetStripeTotalAsync( + [FromRoute] string? shoppingCartId, + [FromServices] IShoppingCartService shoppingCartService, + [FromServices] IStripePaymentService stripePaymentService + ) + { + var shoppingCartViewModel = await shoppingCartService.GetAsync(shoppingCartId); + var total = shoppingCartViewModel.Totals.Single(); + return TypedResults.Ok(new + { + Amount = stripePaymentService.GetPaymentAmount(total), + total.Currency, + }); + } + public static IEndpointRouteBuilder AddStripePublicKeyEndpoint(this IEndpointRouteBuilder builder) { builder.MapGet("api/checkout/stripe/public-key", GetStripePublicKeyAsync) - .RequireAuthorization("Api") .DisableAntiforgery(); return builder; @@ -41,7 +73,6 @@ HttpContext httpContext public static IEndpointRouteBuilder AddStripePaymentIntentEndpoint(this IEndpointRouteBuilder builder) { builder.MapPost("api/checkout/stripe/payment-intent", CreatePaymentIntentAsync) - .RequireAuthorization("Api") .DisableAntiforgery(); return builder; @@ -49,22 +80,20 @@ public static IEndpointRouteBuilder AddStripePaymentIntentEndpoint(this IEndpoin [HttpPost] public static async Task CreatePaymentIntentAsync( - [FromBody] string amount, - [FromServices] PaymentIntentService paymentIntentService + [FromBody] CreatePaymentIntentViewModel viewModel, + [FromServices] ISiteService siteService, + [FromServices] IStripePaymentService stripePaymentService, + [FromServices] IShoppingCartService shoppingCartService, + [FromServices] IEnumerable paymentProviders ) { - var paymentIntent = await paymentIntentService.CreateAsync(new PaymentIntentCreateOptions - { - Amount = long.Parse(amount), - Currency = "eur", - AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions { Enabled = true, }, - }); + var shoppingCartViewModel = await shoppingCartService.GetAsync(viewModel.ShoppingCartId); + var total = shoppingCartViewModel.Totals.Single(); + var paymentIntent = await stripePaymentService.CreatePaymentIntentAsync(total); return TypedResults.Ok(new { clientSecret = paymentIntent.ClientSecret, - // [DEV]: For demo purposes only, you should avoid exposing the PaymentIntent ID in the client-side code. - dpmCheckerLink = $"https://dashboard.stripe.com/settings/payment_methods/review?transaction_id={paymentIntent.Id}", }); } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/CreatePaymentIntentViewModel.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/CreatePaymentIntentViewModel.cs new file mode 100644 index 000000000..a04b84411 --- /dev/null +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/EndPoints/Models/CreatePaymentIntentViewModel.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Commerce.Payment.Stripe.EndPoints.Models; + +public class CreatePaymentIntentViewModel +{ + public string ShoppingCartId { get; set; } +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj index 7359308f4..1eaad2231 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/OrchardCore.Commerce.Payment.Stripe.csproj @@ -48,8 +48,5 @@ - - - diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs index a6c02fc22..52b41281a 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/PaymentIntentPersistence.cs @@ -5,16 +5,37 @@ namespace OrchardCore.Commerce.Payment.Stripe.Services; public class PaymentIntentPersistence : IPaymentIntentPersistence { - private const string PaymentIntentKey = "OrchardCore:Commerce:PaymentIntent"; + // Using _ as a separator to avoid separator character conflicts. + private const string PaymentIntentKey = "OrchardCore_Commerce_PaymentIntent"; private readonly IHttpContextAccessor _httpContextAccessor; private ISession Session => _httpContextAccessor.HttpContext?.Session; public PaymentIntentPersistence(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - public string Retrieve() => Session.GetString(PaymentIntentKey); + public string Retrieve() + { + var serialized = Session.GetString(PaymentIntentKey); + if (serialized == null && _httpContextAccessor.HttpContext != null) + { + _httpContextAccessor.HttpContext.Request.Cookies.TryGetValue(PaymentIntentKey, out var serializedCart); + return serializedCart; + } - public void Store(string paymentIntentId) => Session.SetString(PaymentIntentKey, paymentIntentId); + return serialized; + } - public void Remove() => Session.Remove(PaymentIntentKey); + public void Store(string paymentIntentId) + { + if (Session.GetString(PaymentIntentKey) == paymentIntentId) return; + + Session.SetString(PaymentIntentKey, paymentIntentId); + _httpContextAccessor.SetCookieForever(PaymentIntentKey, paymentIntentId); + } + + public void Remove() + { + Session.Remove(PaymentIntentKey); + _httpContextAccessor.HttpContext?.Response.Cookies.Delete(PaymentIntentKey); + } } diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs index be3ba379f..3eed8e095 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Services/StripePaymentService.cs @@ -304,7 +304,7 @@ await _contentManager.GetAsync(orderId) is not { } order) }; } - private static long GetPaymentAmount(Amount total) + public long GetPaymentAmount(Amount total) { if (CurrencyCollectionConstants.ZeroDecimalCurrencies.Contains(total.Currency.CurrencyIsoCode)) { diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs index 6b78735a0..ee2e9d501 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Startup.cs @@ -49,5 +49,7 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => routes.AddStripeMiddlewareEndpoint() .AddStripePublicKeyEndpoint() - .AddStripePaymentIntentEndpoint(); + .AddStripePaymentIntentEndpoint() + .AddStripeTotalEndpoint() + ; } diff --git a/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs b/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs index 7d16e7fb8..624194d5b 100644 --- a/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs +++ b/src/Modules/OrchardCore.Commerce/Endpoints/Api/ShoppingCartLineEndpoint.cs @@ -9,7 +9,6 @@ using OrchardCore.Commerce.Endpoints.Permissions; using OrchardCore.Commerce.Endpoints.ViewModels; using OrchardCore.Modules; -using System; using System.Threading.Tasks; namespace OrchardCore.Commerce.Endpoints.Api; @@ -17,7 +16,7 @@ public static class ShoppingCartLineEndpoint { public static IEndpointRouteBuilder AddGetCartEndpoint(this IEndpointRouteBuilder builder) { - builder.MapGet("api/shoppingcart/get-cart/{shoppingCartId}", GetCartAsync) + builder.MapGet("api/shoppingcart/get-cart/{shoppingCartId?}", GetCartAsync) .DisableAntiforgery(); return builder; @@ -25,7 +24,7 @@ public static IEndpointRouteBuilder AddGetCartEndpoint(this IEndpointRouteBuilde [Authorize(AuthenticationSchemes = "Api")] private static async Task GetCartAsync( - [FromRoute] string shoppingCartId, + [FromRoute] string? shoppingCartId, [FromServices] IAuthorizationService authorizationService, [FromServices] IShoppingCartService shoppingCartService, HttpContext httpContext) @@ -65,8 +64,6 @@ private static async Task AddItemAsync( return httpContext.ChallengeOrForbid("Api"); } - if (string.IsNullOrEmpty(addItemVM.ShoppingCartId)) { addItemVM.ShoppingCartId = Guid.NewGuid().ToString("n"); } - var errored = await shoppingCartService.AddItemAsync(addItemVM.Line, addItemVM.Token, addItemVM.ShoppingCartId); if (string.IsNullOrEmpty(errored)) { diff --git a/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs b/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs index 4631dd035..01119796d 100644 --- a/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs +++ b/src/Modules/OrchardCore.Commerce/Services/SessionShoppingCartPersistence.cs @@ -23,8 +23,17 @@ public SessionShoppingCartPersistence( _shoppingCartSerializer = shoppingCartSerializer; } - protected override Task RetrieveInnerAsync(string key) => - _shoppingCartSerializer.DeserializeAsync(Session.GetString(key)); + protected override Task RetrieveInnerAsync(string key) + { + var serialized = Session.GetString(key); + if (serialized == null && _httpContextAccessor.HttpContext != null) + { + _httpContextAccessor.HttpContext.Request.Cookies.TryGetValue(key, out var serializedCart); + return _shoppingCartSerializer.DeserializeAsync(serializedCart); + } + + return _shoppingCartSerializer.DeserializeAsync(serialized); + } protected override async Task StoreInnerAsync(string key, ShoppingCart items) { @@ -32,6 +41,8 @@ protected override async Task StoreInnerAsync(string key, ShoppingCart ite if (Session.GetString(key) == cartString) return false; Session.SetString(key, cartString); + _httpContextAccessor.SetCookieForever(key, cartString); + return true; } } diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs index d5584cd38..29062b5c1 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartPersistenceBase.cs @@ -9,7 +9,8 @@ namespace OrchardCore.Commerce.Services; public abstract class ShoppingCartPersistenceBase : IShoppingCartPersistence { - private const string ShoppingCartPrefix = "OrchardCore:Commerce:ShoppingCart"; + // Using _ as a separator to avoid separator character conflicts. + private const string ShoppingCartPrefix = "OrchardCore_Commerce_ShoppingCart"; private readonly Dictionary _scopeCache = []; @@ -68,5 +69,5 @@ public async Task StoreAsync(ShoppingCart items) protected abstract Task StoreInnerAsync(string key, ShoppingCart items); protected string GetCacheId(string shoppingCartId) => - string.IsNullOrEmpty(shoppingCartId) ? ShoppingCartPrefix : $"{ShoppingCartPrefix}:{shoppingCartId}"; + string.IsNullOrEmpty(shoppingCartId) ? ShoppingCartPrefix : $"{ShoppingCartPrefix}_{shoppingCartId}"; }