Browse Source

Merge pull request #45 from abpframework/gterdem/cart_merge

Merge anonymous user cart with signed in user cart
pull/46/head
Galip Tolga Erdem 5 years ago
committed by GitHub
parent
commit
52ef82864a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      apps/auth-server/src/EShopOnAbp.AuthServer/AnonymousUserItem.cs
  2. 10
      apps/auth-server/src/EShopOnAbp.AuthServer/EShopOnAbpAuthServerModule.cs
  3. 69
      apps/auth-server/src/EShopOnAbp.AuthServer/EShopUserClaimsPrincipalFactory.cs
  4. 39
      apps/public-web/src/EShopOnAbp.PublicWeb/Basket/UserBasketProvider.cs
  5. 61
      apps/public-web/src/EShopOnAbp.PublicWeb/Components/Basket/Default.cshtml
  6. 8
      apps/public-web/src/EShopOnAbp.PublicWeb/CookieConstants.cs
  7. 7
      apps/public-web/src/EShopOnAbp.PublicWeb/Pages/Basket.cshtml
  8. 2
      apps/public-web/src/EShopOnAbp.PublicWeb/Pages/index.js
  9. 7
      apps/public-web/src/EShopOnAbp.PublicWeb/wwwroot/client-proxies/basket-proxy.js
  10. 2
      apps/public-web/src/EShopOnAbp.PublicWeb/wwwroot/components/basket-widget.js
  11. 1
      services/basket/src/EShopOnAbp.BasketService.Application.Contracts/IBasketAppService.cs
  12. 37
      services/basket/src/EShopOnAbp.BasketService.Application/BasketAppService.cs
  13. 5
      services/basket/src/EShopOnAbp.BasketService.HttpApi.Client/ClientProxies/BasketClientProxy.Generated.cs
  14. 15
      services/basket/src/EShopOnAbp.BasketService.HttpApi.Client/ClientProxies/basket-generate-proxy.json
  15. 7
      shared/EShopOnAbp.Shared.Hosting.AspNetCore/EShopConstants.cs

6
apps/auth-server/src/EShopOnAbp.AuthServer/AnonymousUserItem.cs

@ -0,0 +1,6 @@
namespace EShopOnAbp.AuthServer;
public class AnonymousUserItem
{
public string AnonymousUserId { get; set; }
}

10
apps/auth-server/src/EShopOnAbp.AuthServer/EShopOnAbpAuthServerModule.cs

@ -99,6 +99,16 @@ namespace EShopOnAbp.AuthServer
options.IsEnabled = true;
});
// context.Services.Replace(ServiceDescriptor.Transient<AbpClaimsService, EshopUserClaimService>());
Configure<AbpClaimsServiceOptions>(options =>
{
options.RequestedClaims.AddRange(new[]{
EShopConstants.AnonymousUserClaimName
});
});
Configure<AbpAuditingOptions>(options =>
{
options.ApplicationName = "AuthServer";

69
apps/auth-server/src/EShopOnAbp.AuthServer/EShopUserClaimsPrincipalFactory.cs

@ -0,0 +1,69 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using EShopOnAbp.Shared.Hosting.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Security.Claims;
using IdentityRole = Volo.Abp.Identity.IdentityRole;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace EShopOnAbp.AuthServer;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(AbpUserClaimsPrincipalFactory))]
public class EShopUserClaimsPrincipalFactory : AbpUserClaimsPrincipalFactory
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<EShopUserClaimsPrincipalFactory> _logger;
private readonly IDistributedCache<AnonymousUserItem, Guid> _cache;
protected HttpContext HttpContext => _httpContextAccessor.HttpContext;
public EShopUserClaimsPrincipalFactory(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options,
ICurrentPrincipalAccessor currentPrincipalAccessor,
IAbpClaimsPrincipalFactory abpClaimsPrincipalFactory,
IHttpContextAccessor httpContextAccessor,
ILogger<EShopUserClaimsPrincipalFactory> logger,
IDistributedCache<AnonymousUserItem, Guid> cache) : base(userManager,
roleManager,
options,
currentPrincipalAccessor,
abpClaimsPrincipalFactory)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_cache = cache;
}
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
var principal = await base.CreateAsync(user);
if (HttpContext.Request.Cookies.ContainsKey(EShopConstants.AnonymousUserClaimName))
{
HttpContext.Request.Cookies.TryGetValue(EShopConstants.AnonymousUserClaimName, out var anonymousUserId);
await _cache.SetAsync(user.Id, new AnonymousUserItem {AnonymousUserId = anonymousUserId}, options: new DistributedCacheEntryOptions()
{
AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(60)
});
_logger.LogInformation($"Cached anonymousUserId:{anonymousUserId}.");
}
var cachedItem = await _cache.GetAsync(user.Id);
_logger.LogInformation($"Retrieved anonymousUserId:{cachedItem.AnonymousUserId} from cache.");
principal.Identities.First()
.AddClaim(new Claim(EShopConstants.AnonymousUserClaimName, cachedItem.AnonymousUserId));
return principal;
}
}

39
apps/public-web/src/EShopOnAbp.PublicWeb/Basket/UserBasketProvider.cs

@ -1,12 +1,11 @@
using Castle.Core.Logging;
using EShopOnAbp.BasketService;
using EShopOnAbp.BasketService;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EShopOnAbp.Shared.Hosting.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.Users;
@ -41,8 +40,13 @@ namespace EShopOnAbp.PublicWeb.Basket
{
try
{
var access_token = await HttpContext.GetTokenAsync("access_token");
logger.LogInformation($"ACCESS_TOKEN:{access_token}");
logger.LogInformation($"*********************");
var id_token = await HttpContext.GetTokenAsync("id_token");
logger.LogInformation($"ID_TOKEN:{id_token}");
// Get anonymous user id from cookie
HttpContext.Request.Cookies.TryGetValue(CookieConstants.AnonymousUserCookieName,
HttpContext.Request.Cookies.TryGetValue(EShopConstants.AnonymousUserClaimName,
out string anonymousUserId);
logger.LogInformation($"========= Anonymous User Id from Cookie:{anonymousUserId} ========= ");
@ -50,8 +54,14 @@ namespace EShopOnAbp.PublicWeb.Basket
if (string.IsNullOrEmpty(anonymousUserId))
{
anonymousUserId = guidGenerator.Create().ToString();
HttpContext.Response.Cookies.Append(CookieConstants.AnonymousUserCookieName, anonymousUserId);
logger.LogInformation($"========= Generated new User Id:{anonymousUserId} ========= APPENDED TO COOKIE ====== ");
HttpContext.Response.Cookies.Append(EShopConstants.AnonymousUserClaimName, anonymousUserId
// , new CookieOptions
// {
// SameSite = SameSiteMode.Lax
// }
);
logger.LogInformation(
$"========= Generated new User Id:{anonymousUserId} ========= APPENDED TO COOKIE ====== ");
}
if (!currentUser.IsAuthenticated)
@ -60,11 +70,20 @@ namespace EShopOnAbp.PublicWeb.Basket
$"========= Get Basket for Anonymous UserId:{anonymousUserId} ========= APPENDED TO COOKIE ====== ");
return await basketAppService.GetByAnonymousUserIdAsync(Guid.Parse(anonymousUserId));
}
else
//TODO: Merge with anonymously stored cart if exist
var userClaimValue = currentUser.FindClaimValue(EShopConstants.AnonymousUserClaimName);
foreach (var claim in currentUser.GetAllClaims())
{
logger.LogInformation($"Claim Type:{claim.Type}-Value:{claim.Value}");
}
if (string.IsNullOrEmpty(userClaimValue))
{
//TODO: Merge with anonymously stored cart if exist
return await basketAppService.GetAsync();
}
return await basketAppService.MergeBasketsAsync();
}
catch (Exception ex)
{
@ -73,4 +92,4 @@ namespace EShopOnAbp.PublicWeb.Basket
}
}
}
}
}

61
apps/public-web/src/EShopOnAbp.PublicWeb/Components/Basket/Default.cshtml

@ -1,4 +1,3 @@
@using EShopOnAbp.Localization
@using Microsoft.Extensions.Localization
@ -10,38 +9,50 @@
@if (!Model.Items.Any())
{
<div class="basket-list">
<p class="text-center"><strong><em>Your basket is empty. Click to a product to add it!</em></strong></p>
<p class="text-center">
<strong>
<em>Your basket is empty. Click to a product to add it!</em>
</strong>
</p>
</div>
}
else
{
<div class="basket-list">
@foreach (var item in Model.Items)
{
<div class="basket-list-item" data-product-id="@item.ProductId">
@if (!item.ImageName.IsNullOrEmpty())
{
<img src="/product-images/@item.ImageName"/>
}
else
<div class="row">
<div class="col-lg-10">
<div class="basket-list">
@foreach (var item in Model.Items)
{
<img src="/product-images/noimage.jpg"/>
<div class="basket-list-item" data-product-id="@item.ProductId">
@if (!item.ImageName.IsNullOrEmpty())
{
<img src="/product-images/@item.ImageName"/>
}
else
{
<img src="/product-images/noimage.jpg"/>
}
@item.ProductName
@if (item.Count > 1)
{
<text> (@("x" + item.Count))</text>
}
- $@item.TotalPrice.ToString("0.00")
<i class="fa fa-remove basket-item-remove" title="@L["RemoveFromBasket"]"></i>
</div>
}
@item.ProductName
@if (item.Count > 1)
{
<text> (@("x" + item.Count))</text>
}
- $@item.TotalPrice.ToString("0.00")
<i class="fa fa-remove basket-item-remove" title="@L["RemoveFromBasket"]"></i>
<hr/>
<div class="basket-total-area">
@L["Total"]: $@Model.TotalPrice.ToString("0.00")
</div>
</div>
}
<hr/>
<div class="basket-total-area">
@L["Total"]: $@Model.TotalPrice.ToString("0.00")
</div>
<div>
<abp-button class="basket-purchase-button" text="@L["Purchase"]" button-type="Primary" />
<div class="col-lg-2">
<div class="row"><h4>Total Selected (@Model.Items.Count)</h4></div>
<div class="row"><h3>$@Model.TotalPrice.ToString("0.00")</h3></div>
<div class="row">
<abp-button size="Large" class="basket-purchase-button" text="@L["Purchase"]" button-type="Primary"/>
</div>
</div>
</div>
}

8
apps/public-web/src/EShopOnAbp.PublicWeb/CookieConstants.cs

@ -1,8 +0,0 @@
using System;
namespace EShopOnAbp.PublicWeb;
public static class CookieConstants
{
public const string AnonymousUserCookieName = "eshop_anonymousId";
}

7
apps/public-web/src/EShopOnAbp.PublicWeb/Pages/Basket.cshtml

@ -5,15 +5,16 @@
@model EShopOnAbp.PublicWeb.Pages.Basket
@section scripts {
<abp-script src="/client-proxies/basket-proxy.js" />
<abp-script src="/Pages/index.js" />
<abp-script src="/client-proxies/basket-proxy.js"/>
<abp-script src="/Pages/index.js"/>
}
@inject IStringLocalizer<EShopOnAbpResource> L
<div class="col-lg-12">
<div class="row">
<h3 class="pt-5 pb-4 text-center">@L["YourBasket"]</h3>
<div id="BasketArea">
@await Component.InvokeAsync(typeof(BasketWidgetViewComponent))
</div>
</div>

2
apps/public-web/src/EShopOnAbp.PublicWeb/Pages/index.js

@ -1,6 +1,6 @@
(function () {
//set anonymous user
let anonymousId = abp.utils.getCookieValue("eshop_anonymousId");
let anonymousId = abp.utils.getCookieValue("anonymous_id");
if (anonymousId != null) {
console.info("Anonymous user set:" + anonymousId);
}

7
apps/public-web/src/EShopOnAbp.PublicWeb/wwwroot/client-proxies/basket-proxy.js

@ -25,6 +25,13 @@
}, ajaxParams));
};
eShopOnAbp.basketService.basket.mergeBaskets = function(ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/basket/basket/merge-baskets',
type: 'POST'
}, ajaxParams));
};
eShopOnAbp.basketService.basket.addProduct = function(input, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/basket/basket/product',

2
apps/public-web/src/EShopOnAbp.PublicWeb/wwwroot/components/basket-widget.js

@ -1,6 +1,6 @@
(function () {
let anonymousId = abp.utils.getCookieValue("eshop_anonymousId");
let anonymousId = abp.utils.getCookieValue("anonymous_id");
abp.widgets.BasketWidget = function ($wrapper) {

1
services/basket/src/EShopOnAbp.BasketService.Application.Contracts/IBasketAppService.cs

@ -8,6 +8,7 @@ public interface IBasketAppService : IApplicationService
{
Task<BasketDto> GetAsync();
Task<BasketDto> GetByAnonymousUserIdAsync(Guid id);
Task<BasketDto> MergeBasketsAsync();
Task<BasketDto> AddProductAsync(AddProductDto input);
Task<BasketDto> RemoveProductAsync(RemoveProductDto input);
Task PurchaseAsync();

37
services/basket/src/EShopOnAbp.BasketService.Application/BasketAppService.cs

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Users;
using EShopOnAbp.CatalogService.Products;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.EventBus.Distributed;
@ -15,7 +16,6 @@ public class BasketAppService : BasketServiceAppService, IBasketAppService
private readonly IBasketRepository _basketRepository;
private readonly IBasketProductService _basketProductService;
private readonly IDistributedEventBus _distributedEventBus;
private Guid _anonymousUserId { get; set; }
public BasketAppService(
IBasketRepository basketRepository,
@ -36,14 +36,43 @@ public class BasketAppService : BasketServiceAppService, IBasketAppService
public async Task<BasketDto> GetByAnonymousUserIdAsync(Guid id)
{
var basket = await _basketRepository.GetAsync(id);
_anonymousUserId = id;
;
return await GetBasketDtoAsync(basket);
}
public async Task<BasketDto> AddProductAsync(AddProductDto input)
public async Task<BasketDto> MergeBasketsAsync()
{
Console.WriteLine("========================= AnonymousId: "+input.AnonymousId);
//TODO: move to custom shared project
var anonymousUserIdString = CurrentUser.FindClaimValue("anonymous_id");
if (!Guid.TryParse(anonymousUserIdString, out Guid anonymousUserId))
{
Logger.LogError($"Couldn't parse anonymous Id from claim!{anonymousUserIdString}");
}
Basket anonymousUserBasket = await _basketRepository.GetAsync(anonymousUserId);
if (!CurrentUser.IsAuthenticated)
{
Logger.LogWarning($"User is not authenticated! Merging baskets failed!");
return await GetBasketDtoAsync(anonymousUserBasket);
}
var userBasket = await _basketRepository.GetAsync(CurrentUser.GetId());
foreach (var item in anonymousUserBasket.Items)
{
userBasket.AddProduct(item.ProductId, item.Count);
}
await _basketRepository.UpdateAsync(userBasket);
anonymousUserBasket.Clear();
await _basketRepository.UpdateAsync(anonymousUserBasket);
return await GetBasketDtoAsync(userBasket);
}
public async Task<BasketDto> AddProductAsync(AddProductDto input)
{
Guid userId = CurrentUser.IsAuthenticated ? CurrentUser.GetId() : input.AnonymousId.GetValueOrDefault();
var basket = await _basketRepository.GetAsync(userId);

5
services/basket/src/EShopOnAbp.BasketService.HttpApi.Client/ClientProxies/BasketClientProxy.Generated.cs

@ -28,6 +28,11 @@ namespace EShopOnAbp.BasketService.ClientProxies
});
}
public virtual async Task<BasketDto> MergeBasketsAsync()
{
return await RequestAsync<BasketDto>(nameof(MergeBasketsAsync));
}
public virtual async Task<BasketDto> AddProductAsync(AddProductDto input)
{
return await RequestAsync<BasketDto>(nameof(AddProductAsync), new ClientProxyRequestTypeValue

15
services/basket/src/EShopOnAbp.BasketService.HttpApi.Client/ClientProxies/basket-generate-proxy.json

@ -75,6 +75,21 @@
"allowAnonymous": null,
"implementFrom": "EShopOnAbp.BasketService.IBasketAppService"
},
"MergeBasketsAsync": {
"uniqueName": "MergeBasketsAsync",
"name": "MergeBasketsAsync",
"httpMethod": "POST",
"url": "api/basket/basket/merge-baskets",
"supportedVersions": [],
"parametersOnMethod": [],
"parameters": [],
"returnValue": {
"type": "EShopOnAbp.BasketService.BasketDto",
"typeSimple": "EShopOnAbp.BasketService.BasketDto"
},
"allowAnonymous": null,
"implementFrom": "EShopOnAbp.BasketService.IBasketAppService"
},
"AddProductAsyncByInput": {
"uniqueName": "AddProductAsyncByInput",
"name": "AddProductAsync",

7
shared/EShopOnAbp.Shared.Hosting.AspNetCore/EShopConstants.cs

@ -0,0 +1,7 @@
namespace EShopOnAbp.Shared.Hosting.AspNetCore;
public static class EShopConstants
{
// public const string AnonymousUserCookieName = "eshop_anonymousId";
public const string AnonymousUserClaimName = "anonymous_id";
}
Loading…
Cancel
Save