diff --git a/README.md b/README.md
index 4685ca4..0003a14 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,24 @@
[![7-day bug-fix policy](https://img.shields.io/badge/-7--days_bug--fixing_policy-grey?labelColor=orange&logo=data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3ZnLWljb24iIHN0eWxlPSJ3aWR0aDogMWVtOyBoZWlnaHQ6IDFlbTt2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO2ZpbGw6IGN1cnJlbnRDb2xvcjtvdmVyZmxvdzogaGlkZGVuOyIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04ODguNDkgMjIyLjY4NnYtMzEuNTRsLTY1LjY3Mi0wLjk1NWgtMC4yMDVhNDY1LjcxNSA0NjUuNzE1IDAgMCAxLTE0NC4zMTUtMzEuMzM0Yy03Ny4wMDUtMzEuMTk4LTEyNi4yOTQtNjYuNzY1LTEyNi43MDMtNjcuMTA3bC0zOS44LTI4LjY3Mi0zOS4xODUgMjguNDY4Yy0yLjA0OCAxLjUwMS00OS45MDMgMzYuMDQ0LTEyNi45MDggNjcuMzFhNDQ3LjQyIDQ0Ny40MiAwIDAgMS0xNDQuNTIgMzEuMzM1bC02NS44NzcgMC45NTZ2Mzc4Ljg4YzAgODcuMDQgNDkuODM0IDE4NC42NjEgMTM3LjAxIDI2Ny44MSAzNy41NDcgMzUuODQgNzkuMjU4IDY2LjM1NSAxMjAuODMzIDg4LjIgNDMuMjggMjIuNzMzIDg0LjI0IDM0LjYxMiAxMTguODUyIDM0LjYxMiAzNC40MDYgMCA3NS43NzYtMTIuMTUyIDExOS42MDMtMzUuMTU4YTU0Ny45NzcgNTQ3Ljk3NyAwIDAgMCAxMjAuMDEzLTg3LjY1NCA1MTUuMjA5IDUxNS4yMDkgMCAwIDAgOTYuMTg4LTEyMi44OGMyNy4xMDItNDkuNTYyIDQwLjgyMy05OC4zMDQgNDAuODIzLTE0NC45OTlsLTAuMTM2LTM0Ny4yMDR6TTUxMC4wOSAxNDMuNDI4bDEuNzA2LTEuMzY1IDEuNzc1IDEuMzY1YzUuODAzIDQuMTY1IDU5LjUyOSA0MS44NDggMTQwLjM1NiA3NC43NTIgNzkuMTkgMzIuMDg2IDE1My42IDM1LjYzNSAxNjcuNjYzIDM2LjA0NWwyLjU5NCAwLjA2OCAwLjIwNSAzMTUuNzM0YzAuMTM3IDY5LjQ5NS00Mi41OTggMTUwLjE4Ni0xMTcuMDc3IDIyMS40NTdDNjQxLjU3IDg1NC4yODkgNTYzLjEzIDg5Ni40NzggNTEyIDg5Ni40NzhjLTIzLjY4OSAwLTU1LjU3LTkuODk5LTg5LjcwMi0yNy43ODVhNDc4LjgyMiA0NzguODIyIDAgMCAxLTEwNS42MDktNzcuMjc4QzI0Mi4yMSA3MjAuMjEzIDE5OS40NzUgNjM5LjUyMiAxOTkuNDc1IDU2OS44OVYyNTQuMjI1bDIuNzMtMC4xMzZjMy4yNzggMCA4Mi42MDQtMS41MDIgMTY3LjY2NC0zNS45NzdhNzM5Ljk0MiA3MzkuOTQyIDAgMCAwIDE0MC4yMi03NC42MTV2LTAuMDY5eiIgIC8+PHBhdGggZD0iTTcxMy4zMTggMzY4LjY0YTMyLjIyMiAzMi4yMjIgMCAwIDAtNDUuMzI5IDBMNDQ5LjE5NSA1ODcuNDM1bC05My4xODQtOTMuMTE2YTMyLjIyMiAzMi4yMjIgMCAwIDAtNDUuMzMgMCAzMi4yMjIgMzIuMjIyIDAgMCAwIDAgNDUuMjZsMTE1Ljg1IDExNS44NWEzMi4yOSAzMi4yOSAwIDAgMCA0NS4zMjggMEw3MTMuMzIgNDEzLjlhMzIuMjIyIDMyLjIyMiAwIDAgMCAwLTQ1LjMzeiIgIC8+PC9zdmc+)](https://github.com/Kentico/.github/blob/main/SUPPORT.md#full-support) [![CI: Build and Test](https://github.com/Kentico/xperience-by-kentico-shopify/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Kentico/xperience-by-kentico-shopify/actions/workflows/ci.yml)
-| Name | Package |
-|----------------------------------- | --------------- |
-| Kentico.Xperience.Shopify | [![NuGet Package](https://img.shields.io/nuget/v/Kentico.Xperience.Shopify.svg)](https://www.nuget.org/packages/Kentico.Xperience.Shopify) |
-| Kentico.Xperience.Shopify.Rcl | [![NuGet Package](https://img.shields.io/nuget/v/Kentico.Xperience.Shopify.Rcl.svg)](https://www.nuget.org/packages/Kentico.Xperience.Shopify.Rcl)|
+| Name | Package |
+| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Kentico.Xperience.Shopify | [![NuGet Package](https://img.shields.io/nuget/v/Kentico.Xperience.Shopify.svg)](https://www.nuget.org/packages/Kentico.Xperience.Shopify) |
+| Kentico.Xperience.Shopify.Rcl | [![NuGet Package](https://img.shields.io/nuget/v/Kentico.Xperience.Shopify.Rcl.svg)](https://www.nuget.org/packages/Kentico.Xperience.Shopify.Rcl) |
## Description
+
This integration connects your Shopify store with the Xperience by Kentico application using [Shopify Storefront](https://shopify.dev/docs/api/storefront) and [Shopify Admin](https://shopify.dev/docs/api/admin) APIs. It provides synchronization of products and e-commerce actions between the two platforms. Implemented features provide users with the options to:
+
- Add, update and remove products in the shopping cart.
- Manage discount coupons.
- Proceed to checkout directly on the Shopify store page.
### Limitations
-**Shopify API can return maximum of 250 items in one API request**. For larger number of products, pagination needs to be implemented. More info can be found in the [Usage-Guide.md](./docs/Usage-Guide.md#limitations).
+
+- **Shopify API can return maximum of 250 items in one API request**. For larger number of products, pagination needs to be implemented. More info can be found in the [Usage-Guide.md](./docs/Usage-Guide.md#limitations).
+
+- Only one currency pre website channel is supported.
## Screenshots
@@ -29,99 +34,145 @@ This integration connects your Shopify store with the Xperience by Kentico appli
Summary of libraries (NuGet packages) used by this integration and their Xperience by Kentico version requirements. To use this integration, your Xperience project must be upgraded to at least the highest version listed.
| Library | Xperience by Kentico Version | Library Version |
-|----------------------------------- |------------------------------| --------------- |
+| ---------------------------------- | ---------------------------- | --------------- |
| Kentico.Xperience.Ecommerce.Common | \>= 29.0.1 | 1.0.0 |
| Kentico.Xperience.Shopify | \>= 29.0.2 | 1.0.0 |
| Kentico.Xperience.Shopify.Rcl | \>= 29.0.2 | 1.0.0 |
### Dependencies
+
- [ASP.NET Core 8.0](https://dotnet.microsoft.com/en-us/download)
- [Xperience by Kentico](https://docs.kentico.com/changelog)
## Quick Start
+
1. Generate Shopify API access tokens (see [Generating Shopify API credentials](./docs/Usage-Guide.md#generating-shopify-api-credentials) for details).
- - [Install](https://shopify.dev/docs/custom-storefronts/building-with-the-storefront-api/getting-started) the [Headless channel](https://shopify.dev/docs/custom-storefronts/getting-started/build-options#the-headless-channel) into your Shopify admin. During the installation, select **Create storefront** and [generate a private Storefront API token](https://shopify.dev/docs/api/usage/authentication#getting-started-with-private-access).
- - Install a [custom application](https://help.shopify.com/en/manual/apps/app-types/custom-apps#create-and-install-a-custom-app) into your Shopify admin and generate a Shopify Admin API access token. Set following access scopes: `write_product_listings`, `read_product_listings`, `write_products`, `read_products`, `read_inventory`, `write_orders`, `read_orders`.
+
+ - [Install](https://shopify.dev/docs/custom-storefronts/building-with-the-storefront-api/getting-started) the [Headless channel](https://shopify.dev/docs/custom-storefronts/getting-started/build-options#the-headless-channel) into your Shopify admin. During the installation, select **Create storefront** and [generate a private Storefront API token](https://shopify.dev/docs/api/usage/authentication#getting-started-with-private-access).
+ - Install a [custom application](https://help.shopify.com/en/manual/apps/app-types/custom-apps#create-and-install-a-custom-app) into your Shopify admin and generate a Shopify Admin API access token. Set following access scopes: `write_product_listings`, `read_product_listings`, `write_products`, `read_products`, `read_inventory`, `write_orders`, `read_orders`.
2. Add these packages to your Xperience by Kentico application using the .NET CLI.
- ```powershell
- dotnet add package Kentico.Xperience.Shopify
- dotnet add package Kentico.Xperience.Shopify.Rcl
- ```
+
+ ```powershell
+ dotnet add package Kentico.Xperience.Shopify
+ dotnet add package Kentico.Xperience.Shopify.Rcl
+ ```
3. Configure your Xperience application to connect to your Shopify instance via the `CMSShopifyConfig` object. For development purposes, you can also configure these settings directly in the Xperience by Kentico administration via the **Shopify integration** application.
- ```json
- {
- "CMSShopifyConfig": {
- "ShopifyUrl": "https://your-shopify-store-url.com/",
- "AdminApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "StorefrontApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "StorefrontApiVersion": "YYYY-MM"
- }
- }
- ```
- **Setting description**
- | Setting | Description |
- | -------------------- | ------------------------------------------------------------------------------------- |
- | ShopifyUrl | URL of the Shopify store |
- | AdminApiToken | Access token for the Admin API calls |
- | StorefrontApiToken | Access token for the Storefront API calls |
- | StorefrontApiVersion | Storefront API version that will be used in API calls. Must use the format: `YYYY-MM` |
-
- Note: The `StorefrontApiVersion` refers to the version of the Shopify Storefront API you are using. You can find the available versions and their release dates in the [Shopify API versioning documentation](https://shopify.dev/docs/api/usage/versioning).
-
- You can also configure the integration via the [Shopify integration](#shopify-configuration-module) application in the Xperience admin UI. However, note that this approach should only be used for development purposes. For the production, use one of the recommended [configuration methods](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration).
-
-4. Add services provided by the integration to the service container.
- ```csharp
- // Program.cs
-
- // Registers Shopify services
- builder.Services.RegisterShopifyServices(builder.Configuration);
- ```
-
-5. Enable session state for the application.
- ```csharp
- // Program.cs
-
- // Enable session state for appliation
- app.UseSession();
- ```
-
-6. Add Xperience objects used by the integration to your project using [Continuous Integration](https://docs.kentico.com/x/FAKQC). The integration's CI files are located under `.\examples\DancingGoat-Shopify\App_Data\CIRepository\`. This CI repository does not contain any other objects than objects related to Shopify integration so it can be merged into your existing CI repository. Copy these files to your Continuous Integration repository and run:
- ```powershell
- dotnet run --no-build --kxp-ci-restore
- ```
+
+ ```json
+ {
+ "CMSShopifyConfig": {
+ "ShopifyUrl": "https://your-shopify-store-url.com/",
+ "AdminApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "StorefrontApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "StorefrontApiVersion": "YYYY-MM"
+ }
+ }
+ ```
+
+ **Setting description**
+ | Setting | Description |
+ | -------------------- | ------------------------------------------------------------------------------------- |
+ | ShopifyUrl | URL of the Shopify store |
+ | AdminApiToken | Access token for the Admin API calls |
+ | StorefrontApiToken | Access token for the Storefront API calls |
+ | StorefrontApiVersion | Storefront API version that will be used in API calls. Must use the format: `YYYY-MM` |
+
+ Note: The `StorefrontApiVersion` refers to the version of the Shopify Storefront API you are using. You can find the available versions and their release dates in the [Shopify API versioning documentation](https://shopify.dev/docs/api/usage/versioning).
+
+ You can also configure the integration via the [Shopify integration](#shopify-configuration-module) application in the Xperience admin UI. However, note that this approach should only be used for development purposes. For the production, use one of the recommended [configuration methods](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration).
+
+4. Configure currency and country code for website channels via the `CMSShopifyWebsiteChannelsConfig` object.
+
+ ```json
+ {
+ "CMSShopifyWebsiteChannelsConfig": {
+ "Settings": [
+ {
+ "ChannelName": "MyWebsiteChannel1",
+ "CurrencyCode": "CZK",
+ "Country": "CZ"
+ },
+ {
+ "ChannelName": "MyWebsiteChannel2",
+ "CurrencyCode": "USD",
+ "Country": "US"
+ }
+ ],
+ "DefaultSetting": {
+ "CurrencyCode": "CZK",
+ "Country": "CZ"
+ }
+ }
+ }
+ ```
+
+ **Setting description**
+ | Setting | Description |
+ |----------------|----------------------------------------------------------------------------------------------------------------|
+ | Settings | List of configurations. Each item contains configuration for specific website channel defined by `ChannelName` |
+ | DefaultSetting | Configuration used if no configuration found in the `Settings` for the given website channel. |
+
+ Note: Only currencies set in the shopify store can be used.
+
+5. Add services provided by the integration to the service container.
+
+ ```csharp
+ // Program.cs
+
+ // Registers Shopify services
+ builder.Services.RegisterShopifyServices(builder.Configuration);
+ ```
+
+6. Enable session state for the application.
+
+ ```csharp
+ // Program.cs
+
+ // Enable session state for appliation
+ app.UseSession();
+ ```
+
+7. Add Xperience objects used by the integration to your project using [Continuous Integration](https://docs.kentico.com/x/FAKQC). The integration's CI files are located under `.\examples\DancingGoat-Shopify\App_Data\CIRepository\`. This CI repository does not contain any other objects than objects related to Shopify integration so it can be merged into your existing CI repository. Copy these files to your Continuous Integration repository and run:
+
+ ```powershell
+ dotnet run --no-build --kxp-ci-restore
+ ```
The command restores the following objects:
- Page content types: Thank you page, Shopping cart page, Shopify product detail page, Store page, Shopify category page(more info in [Usage-Guide.md](./docs/Usage-Guide.md#e-commerce-content-types).
- Content types used for [synchronization with Shopify](./docs/Usage-Guide.md#shopify-products-synchronization): Shopify product, Shopify Product Variant, Shopify Image(more info in [Usage-Guide.md](./docs/Usage-Guide.md#e-commerce-content-types).
- [Shopify integration module](#shopify-configuration-module) for setting API credentials and adding currency codes.
- Custom activities: Product added to shopping cart, Product removed from shopping cart, Purchase, Purchased product.
- Using CD is not recommended for restoring `Product detail`, `Category`, `Store` pages and content items that were created by [synchronization with Shopify](./docs/Usage-Guide.md#shopify-products-synchronization). This is because the `Shopify product` content item is connected to Product detail page. Therefore, both Product detail page and Shopify product content item will be restored. However, the synchronization already created the same Shopify product content item, using CD restore will result in duplicate Shopify product content items. To filter these objects from continuous deployment, add following rule into `repository.config`:
- ```xml
-
-
- Shopify.Image;Shopify.StorePage;Shopify.Product;Shopify.ProductDetailPage;Shopify.ProductVariant;Shopify.CategoryPage
-
- ```
+ Using CD is not recommended for restoring `Product detail`, `Category`, `Store` pages and content items that were created by [synchronization with Shopify](./docs/Usage-Guide.md#shopify-products-synchronization). This is because the `Shopify product` content item is connected to Product detail page. Therefore, both Product detail page and Shopify product content item will be restored. However, the synchronization already created the same Shopify product content item, using CD restore will result in duplicate Shopify product content items. To filter these objects from continuous deployment, add following rule into `repository.config`:
+
+ ```xml
+
+
+ Shopify.Image;Shopify.StorePage;Shopify.Product;Shopify.ProductDetailPage;Shopify.ProductVariant;Shopify.CategoryPage
+
+ ```
-7. The integration uses a **Products** [Page Builder](https://docs.kentico.com/x/6QWiCQ) widget to display products from your Shopify store. Since Page Builder widgets cannot be distributed as part of NuGet packages, you must copy the **Products** widget implementation from the example project in this repository to your project. The widget implementation is located in [this folder](./examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget).
+8. The integration uses a **Products** [Page Builder](https://docs.kentico.com/x/6QWiCQ) widget to display products from your Shopify store. Since Page Builder widgets cannot be distributed as part of NuGet packages, you must copy the **Products** widget implementation from the example project in this repository to your project. The widget implementation is located in [this folder](./examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget).
-8. Run your Xperience application (e.g., `using dotnet run`).
+9. Run your Xperience application (e.g., `using dotnet run`).
-9. In the Xperience admin UI, open the [Shopify integration](#shopify-configuration-module) application and on the **Shopify currencies formats** tab and add the required currencies for your store. For currency codes, use the values from [CurrencyCodeEnum](https://shopify.dev/docs/api/storefront/2024-01/enums/CurrencyCode). To format the output, we recommend using [custom numeric format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
+10. In the Xperience admin UI, open the [Shopify integration](#shopify-configuration-module) application and on the **Shopify currencies formats** tab and add the required currencies for your store. For currency codes, use the values from [CurrencyCodeEnum](https://shopify.dev/docs/api/storefront/2024-01/enums/CurrencyCode). To format the output, we recommend using [custom numeric format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
## Full Instructions
View the [Usage Guide](./docs/Usage-Guide.md) for more detailed instructions.
## Shopify configuration module
+
The integration adds a new **Shopify integration** application to the admin UI. Using the application, administrators can set Shopify API credentials and add currency formats. If Shopify API credentials are provided both via this application and configuration providers (e.g., appsettings.json), values from the configuration will take precedence. The application is located under the `Configuration` category.
![Shopify integration module overview](./images/screenshots/shopify_integration_module.jpg "Shopify integration module overview")
## Codebase overview
+
Repository contains solution with Xperience by Kentico integration to Shopify. It shows the connection to the Shopify headless API and shows the implementation of a simple e-shop on Xperience by Kentico (extended Dancing Goat sample site). The solution consists of these parts:
+
- Kentico.Xperience.Shopify - class library that contains all services necessary for this integration.
- Kentico.Xperience.Shopify.Rcl - razor class library for selector components(used in standalone product listing widget).
- DancingGoat - Sample Dancing Goat site.
diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md
index 2e5bd80..9764a97 100644
--- a/docs/Usage-Guide.md
+++ b/docs/Usage-Guide.md
@@ -11,7 +11,8 @@ Class library consists of 2 main parts - Shopify products synchronization and th
The **Product listing** widget is a standalone widget that displays Shopify products using the Shopify Admin REST API. The Shopify integration must be set up in `appsettings.json` or in the `Shopify integration` application for this widget to work (no products need to be synchronized). The widget can be used to display products from your Shopify store outside of the [content type structure](#integration-specific-content-types) the integration provides.
#### Limitations
-Shopify API can return maximum of 250 items in one API request. For larger number of products, pagination needs to be implemented. Detailed information on Shopify API pagination can be found [here](https://shopify.dev/docs/api/usage/pagination-rest). The `ShopifyProductService` wraps result in [ListResultWrapper](../src/Kentico.Xperience.Shopify/Products/Models/ListResultWrapper.cs). This wrapper returns retrieved items along with next page and previous page [PagingFilterParams](../src/Kentico.Xperience.Shopify/Products/Models/PagingFilterParams.cs). These filters can then be used in the `GetProductsAsync` method parameters to retrieve next or previous page from Shopify API. Due to this limitation, the maximum number of retrieved results in the [product listing widget](#product-listing-widget) is 250. To increase this limit, pagination must be implemented in the widget. This limitation also affects product synchronization, where only first 250 products are synchronized, and shopping cart, which can have maximum of 250 cart items.
+- Shopify API can return maximum of 250 items in one API request. For larger number of products, pagination needs to be implemented. Detailed information on Shopify API pagination can be found [here](https://shopify.dev/docs/api/usage/pagination-rest). The `ShopifyProductService` wraps result in [ListResultWrapper](../src/Kentico.Xperience.Shopify/Products/Models/ListResultWrapper.cs). This wrapper returns retrieved items along with next page and previous page [PagingFilterParams](../src/Kentico.Xperience.Shopify/Products/Models/PagingFilterParams.cs). These filters can then be used in the `GetProductsAsync` method parameters to retrieve next or previous page from Shopify API. Due to this limitation, the maximum number of retrieved results in the [product listing widget](#product-listing-widget) is 250. To increase this limit, pagination must be implemented in the widget. This limitation also affects product synchronization, where only first 250 products are synchronized, and shopping cart, which can have maximum of 250 cart items.
+- Only one currency per website channel is supported.
### Shopify products synchronization
Synchronization running in background thread worker periodically every 15 minutes and all synchronization items are stored as following content items:
diff --git a/examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget/ShopifyProductListWidgetProperties.cs b/examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget/ShopifyProductListWidgetProperties.cs
index 50923c2..4e529b2 100644
--- a/examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget/ShopifyProductListWidgetProperties.cs
+++ b/examples/DancingGoat-Shopify/Components/Widgets/Shopify/ProductListWidget/ShopifyProductListWidgetProperties.cs
@@ -12,7 +12,6 @@ public class ShopifyProductListWidgetProperties : IWidgetProperties
[EditingComponent(TextInputComponent.IDENTIFIER, Label = "Title", Order = 0)]
public string Title { get; set; }
- // TODO - create selector for long datatype
[EditingComponent(ShopifyCollectionSelectorComponent.IDENTIFIER, Label = "Collection", Order = 10)]
public string CollectionID { get; set; }
diff --git a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyCategoryController.cs b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyCategoryController.cs
index fb8d92a..77ac5f9 100644
--- a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyCategoryController.cs
+++ b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyCategoryController.cs
@@ -7,6 +7,7 @@
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
+using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products;
using Kentico.Xperience.Shopify.Products.Models;
@@ -28,6 +29,7 @@ public class ShopifyCategoryController : Controller
private readonly ISettingsService settingsService;
private readonly IConversionService conversionService;
private readonly ILogger logger;
+ private readonly IShopifyIntegrationSettingsService shopifySettingsService;
public ShopifyCategoryController(
CategoryPageRepository categoryPageRepository,
@@ -37,7 +39,8 @@ public ShopifyCategoryController(
IProgressiveCache progressiveCache,
ISettingsService settingsService,
IConversionService conversionService,
- ILogger logger)
+ ILogger logger,
+ IShopifyIntegrationSettingsService shopifySettingsService)
{
this.categoryPageRepository = categoryPageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
@@ -47,6 +50,7 @@ public ShopifyCategoryController(
this.settingsService = settingsService;
this.conversionService = conversionService;
this.logger = logger;
+ this.shopifySettingsService = shopifySettingsService;
}
public async Task Index()
{
@@ -62,7 +66,9 @@ public async Task Index()
async (_) => await GetProductPrices(products.Select(x => x.Product.FirstOrDefault())),
new CacheSettings(cacheMinutes, webPage.WebsiteChannelName, webPage.LanguageName, categoryPage.SystemFields.WebPageItemGUID));
- return View(CategoryPageViewModel.GetViewModel(categoryPage, prices, products, urls, logger));
+ string currencyCode = shopifySettingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty;
+
+ return View(CategoryPageViewModel.GetViewModel(categoryPage, prices, products, urls, logger, currencyCode));
}
private async Task> GetProductPrices(IEnumerable productContentItems)
diff --git a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs
index 65e5b58..a91db24 100644
--- a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs
+++ b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyProductDetailController.cs
@@ -5,10 +5,12 @@
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
+using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products.Models;
using Kentico.Xperience.Shopify.ShoppingCart;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
using Shopify.Controllers;
@@ -24,28 +26,33 @@ public class ShopifyProductDetailController : Controller
private readonly ProductDetailPageRepository productDetailPageRepository;
private readonly IWebPageDataContextRetriever webPageDataContextRetriever;
private readonly IShoppingService shoppingService;
+ private readonly IShopifyIntegrationSettingsService settingsService;
public ShopifyProductDetailController(ProductDetailPageRepository productDetailPageRepository,
IWebPageDataContextRetriever webPageDataContextRetriever,
- IShoppingService shoppingService)
+ IShoppingService shoppingService,
+ IShopifyIntegrationSettingsService settingsService)
{
this.productDetailPageRepository = productDetailPageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
this.shoppingService = shoppingService;
+ this.settingsService = settingsService;
}
[HttpGet]
public async Task Index(string variantID = null)
{
- // TODO - dynamic resolve country
- string country = "CZ";
- string currency = "CZK";
-
if (!TempData.TryGetValue(ERROR_MESSAGES_KEY, out object tempDataErrors) || tempDataErrors is not string[] errorMessages)
{
errorMessages = [];
}
+ var config = settingsService.GetWebsiteChannelSettings();
+ if (config == null)
+ {
+ return View(new ProductDetailViewModel());
+ }
+
var webPage = webPageDataContextRetriever.Retrieve().WebPage;
var productDetail = await productDetailPageRepository.GetProductDetailPage(webPage.WebPageItemID, webPage.LanguageName, HttpContext.RequestAborted);
@@ -54,7 +61,7 @@ public async Task Index(string variantID = null)
return View(new ProductDetailViewModel());
}
- return View(ProductDetailViewModel.GetViewModel(productDetail, variantID ?? string.Empty, country, currency, errorMessages));
+ return View(ProductDetailViewModel.GetViewModel(productDetail, variantID ?? string.Empty, config.Country, config.CurrencyCode, errorMessages));
}
[HttpPost]
diff --git a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyStoreController.cs b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyStoreController.cs
index 742e0a1..d3d42cf 100644
--- a/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyStoreController.cs
+++ b/examples/DancingGoat-Shopify/Controllers/Shopify/ShopifyStoreController.cs
@@ -9,6 +9,7 @@
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
+using Kentico.Xperience.Shopify.Config;
using Kentico.Xperience.Shopify.Products;
using Kentico.Xperience.Shopify.Products.Models;
@@ -32,6 +33,7 @@ public class ShopifyStoreController : Controller
private readonly ISettingsService settingsService;
private readonly IConversionService conversionService;
private readonly IShopifyPriceService priceService;
+ private readonly IShopifyIntegrationSettingsService shopifySettingsService;
public ShopifyStoreController(StorePageRepository storePageRepository,
IWebPageDataContextRetriever webPageDataContextRetriever,
@@ -42,7 +44,8 @@ public ShopifyStoreController(StorePageRepository storePageRepository,
IProgressiveCache progressiveCache,
ISettingsService settingsService,
IConversionService conversionService,
- IShopifyPriceService priceService)
+ IShopifyPriceService priceService,
+ IShopifyIntegrationSettingsService shopifySettingsService)
{
this.storePageRepository = storePageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
@@ -54,6 +57,7 @@ public ShopifyStoreController(StorePageRepository storePageRepository,
this.settingsService = settingsService;
this.conversionService = conversionService;
this.priceService = priceService;
+ this.shopifySettingsService = shopifySettingsService;
}
public async Task Index()
@@ -86,7 +90,9 @@ private async Task> GetProductListV
if (!string.IsNullOrEmpty(shopifyProductId) && prices.TryGetValue(shopifyProductId, out var price))
{
- productViewModels.Add(ShopifyProductListItemViewModel.GetViewModel(productPage.Product.FirstOrDefault(), url, price));
+ string currencyCode = shopifySettingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty;
+
+ productViewModels.Add(ShopifyProductListItemViewModel.GetViewModel(productPage.Product.FirstOrDefault(), url, price, currencyCode));
}
}
diff --git a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/CategoryPage/CategoryPageViewModel.cs b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/CategoryPage/CategoryPageViewModel.cs
index 92ff531..bc4e2cd 100644
--- a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/CategoryPage/CategoryPageViewModel.cs
+++ b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/CategoryPage/CategoryPageViewModel.cs
@@ -14,7 +14,8 @@ public static CategoryPageViewModel GetViewModel(
IDictionary productPrices,
IEnumerable products,
IDictionary productUrls,
- ILogger logger)
+ ILogger logger,
+ string currencyCode)
{
var productListItems = new List();
foreach (var product in products)
@@ -25,7 +26,7 @@ public static CategoryPageViewModel GetViewModel(
}
else
{
- productListItems.Add(GetProductListItem(product, productUrls, productPrices));
+ productListItems.Add(GetProductListItem(product, productUrls, productPrices, currencyCode));
}
}
var model = new CategoryPageViewModel
@@ -37,7 +38,11 @@ public static CategoryPageViewModel GetViewModel(
return model;
}
- private static ShopifyProductListItemViewModel GetProductListItem(ProductDetailPage productPage, IDictionary productUrls, IDictionary productPrices)
+ private static ShopifyProductListItemViewModel GetProductListItem(
+ ProductDetailPage productPage,
+ IDictionary productUrls,
+ IDictionary productPrices,
+ string currencyCode)
{
var product = productPage.Product.FirstOrDefault();
if (product == null)
@@ -47,6 +52,6 @@ private static ShopifyProductListItemViewModel GetProductListItem(ProductDetailP
productUrls.TryGetValue(productPage.SystemFields.WebPageItemGUID, out var url);
productPrices.TryGetValue(product.ShopifyProductID, out var productPriceModel);
- return ShopifyProductListItemViewModel.GetViewModel(product, url, productPriceModel);
+ return ShopifyProductListItemViewModel.GetViewModel(product, url, productPriceModel, currencyCode);
}
}
diff --git a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/_Shared/ShopifyProductListItemViewModel.cs b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/_Shared/ShopifyProductListItemViewModel.cs
index 64d20ec..453069e 100644
--- a/examples/DancingGoat-Shopify/Models/WebPage/Shopify/_Shared/ShopifyProductListItemViewModel.cs
+++ b/examples/DancingGoat-Shopify/Models/WebPage/Shopify/_Shared/ShopifyProductListItemViewModel.cs
@@ -16,10 +16,9 @@ public record ShopifyProductListItemViewModel
public string ListPrice { get; init; }
public bool HasMultipleVariants { get; init; }
- public static ShopifyProductListItemViewModel GetViewModel(Product product, WebPageUrl productUrl, ProductPriceModel priceModel)
+ public static ShopifyProductListItemViewModel GetViewModel(Product product, WebPageUrl productUrl, ProductPriceModel priceModel, string currencyCode)
{
- // TODO resolve currency
- string currency = "CZK";
+ string currency = currencyCode;
var mainImage = product.Images.FirstOrDefault() ?? product.Variants.FirstOrDefault(x => x.Image.Any())?.Image.FirstOrDefault();
return new ShopifyProductListItemViewModel()
{
diff --git a/examples/DancingGoat-Shopify/appsettings.json b/examples/DancingGoat-Shopify/appsettings.json
index 67e5137..664bc3b 100644
--- a/examples/DancingGoat-Shopify/appsettings.json
+++ b/examples/DancingGoat-Shopify/appsettings.json
@@ -19,5 +19,23 @@
"AdminApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"StorefrontApiToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"StorefrontApiVersion": "YYYY-MM"
+ },
+ "CMSShopifyWebsiteChannelsConfig": {
+ "Settings": [
+ {
+ "ChannelName": "MyWebsiteChannel1",
+ "CurrencyCode": "CZK",
+ "Country": "CZ"
+ },
+ {
+ "ChannelName": "MyWebsiteChannel2",
+ "CurrencyCode": "USD",
+ "Country": "US"
+ }
+ ],
+ "DefaultSetting": {
+ "CurrencyCode": "CZK",
+ "Country": "CZ"
+ }
}
}
\ No newline at end of file
diff --git a/src/Kentico.Xperience.Shopify.Rcl/Views/Shared/DisplayTemplates/ProductListItem.cshtml b/src/Kentico.Xperience.Shopify.Rcl/Views/Shared/DisplayTemplates/ProductListItem.cshtml
index ebfcb54..1aac45f 100644
--- a/src/Kentico.Xperience.Shopify.Rcl/Views/Shared/DisplayTemplates/ProductListItem.cshtml
+++ b/src/Kentico.Xperience.Shopify.Rcl/Views/Shared/DisplayTemplates/ProductListItem.cshtml
@@ -3,7 +3,7 @@
@{
var price = Model.PriceDetail;
- var currencyFormat = "0:00#";//TODO;
+ var currencyFormat = "0:00#";
}
diff --git a/src/Kentico.Xperience.Shopify/Config/IShopifyIntegrationSettingsService.cs b/src/Kentico.Xperience.Shopify/Config/IShopifyIntegrationSettingsService.cs
index e1dc668..cd75ee2 100644
--- a/src/Kentico.Xperience.Shopify/Config/IShopifyIntegrationSettingsService.cs
+++ b/src/Kentico.Xperience.Shopify/Config/IShopifyIntegrationSettingsService.cs
@@ -11,5 +11,13 @@ public interface IShopifyIntegrationSettingsService
///
/// containing the settings or NULL if no configuration is found.
ShopifyConfig? GetSettings();
+
+ ///
+ /// Get current website channel configuration from appsettings.
+ ///
+ ///
+ /// containing configuration for current website channel or default value if no configuration is found.
+ ///
+ ShopifyWebsiteChannelConfig? GetWebsiteChannelSettings();
}
}
diff --git a/src/Kentico.Xperience.Shopify/Config/ShopifyIntegrationSettingsService.cs b/src/Kentico.Xperience.Shopify/Config/ShopifyIntegrationSettingsService.cs
index 4c969b4..c80ad6e 100644
--- a/src/Kentico.Xperience.Shopify/Config/ShopifyIntegrationSettingsService.cs
+++ b/src/Kentico.Xperience.Shopify/Config/ShopifyIntegrationSettingsService.cs
@@ -1,5 +1,6 @@
using CMS.DataEngine;
using CMS.Helpers;
+using CMS.Websites.Routing;
using Kentico.Xperience.Shopify.Admin;
@@ -10,26 +11,30 @@ namespace Kentico.Xperience.Shopify.Config
internal class ShopifyIntegrationSettingsService : IShopifyIntegrationSettingsService
{
private readonly IProgressiveCache cache;
- private readonly IOptionsMonitor monitor;
+ private readonly ShopifyConfig shopifyConfig;
private readonly IInfoProvider integrationSettingsProvider;
+ private readonly ShopifyWebsiteChannelConfigOptions websiteChannelConfig;
+ private readonly IWebsiteChannelContext websiteChannelContext;
public ShopifyIntegrationSettingsService(
IProgressiveCache cache,
- IOptionsMonitor monitor,
- IInfoProvider integrationSettingsProvider)
+ IOptionsMonitor shopifyConfigMonitor,
+ IInfoProvider integrationSettingsProvider,
+ IOptionsMonitor websiteChannelConfigMonitor,
+ IWebsiteChannelContext websiteChannelContext)
{
this.cache = cache;
- this.monitor = monitor;
this.integrationSettingsProvider = integrationSettingsProvider;
+ this.websiteChannelContext = websiteChannelContext;
+ shopifyConfig = shopifyConfigMonitor.CurrentValue;
+ websiteChannelConfig = websiteChannelConfigMonitor.CurrentValue;
}
public ShopifyConfig? GetSettings()
{
- var monitorValue = monitor.CurrentValue;
-
- if (ShopifyConfigIsFilled(monitorValue))
+ if (ShopifyConfigIsFilled(shopifyConfig))
{
- return monitorValue;
+ return shopifyConfig;
}
return cache.Load(cs => GetConfigFromSettings(),
@@ -40,6 +45,21 @@ public ShopifyIntegrationSettingsService(
}
+ public ShopifyWebsiteChannelConfig? GetWebsiteChannelSettings()
+ {
+ if (websiteChannelConfig == null)
+ {
+ return null;
+ }
+
+ string? currentChannel = websiteChannelContext.WebsiteChannelName;
+ if (string.IsNullOrEmpty(currentChannel))
+ {
+ return websiteChannelConfig.DefaultSetting;
+ }
+ return websiteChannelConfig.Settings?.Find(x => x.ChannelName == currentChannel) ?? websiteChannelConfig.DefaultSetting;
+ }
+
private ShopifyConfig? GetConfigFromSettings()
{
var settingsInfo = integrationSettingsProvider.Get()
diff --git a/src/Kentico.Xperience.Shopify/Config/ShopifyWebsiteChannelConfigOptions.cs b/src/Kentico.Xperience.Shopify/Config/ShopifyWebsiteChannelConfigOptions.cs
new file mode 100644
index 0000000..3a5a25a
--- /dev/null
+++ b/src/Kentico.Xperience.Shopify/Config/ShopifyWebsiteChannelConfigOptions.cs
@@ -0,0 +1,52 @@
+namespace Kentico.Xperience.Shopify.Config
+{
+ ///
+ /// Class with list of website channel configurations.
+ ///
+ public class ShopifyWebsiteChannelConfigOptions
+ {
+ ///
+ /// The name of configuration section.
+ ///
+ public const string SECTION_NAME = "CMSShopifyWebsiteChannelsConfig";
+
+ ///
+ /// Website channel configurations list.
+ ///
+ public required List Settings { get; set; }
+
+ ///
+ /// Default setting used if no setting for current website channel is found.
+ ///
+ public required ShopifyWebsiteChannelConfig? DefaultSetting { get; set; }
+ }
+
+
+ ///
+ /// Class for website channel configuration
+ ///
+ public class ShopifySpecificWebsiteChannelConfig : ShopifyWebsiteChannelConfig
+ {
+ ///
+ /// Website channel name.
+ ///
+ public required string ChannelName { get; set; }
+ }
+
+
+ ///
+ /// Class for default channel configuration.
+ ///
+ public class ShopifyWebsiteChannelConfig
+ {
+ ///
+ /// ISO 4217 currency code.
+ ///
+ public required string CurrencyCode { get; set; }
+
+ ///
+ /// Two letter country code.
+ ///
+ public required string Country { get; set; }
+ }
+}
diff --git a/src/Kentico.Xperience.Shopify/Extensions/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.Shopify/Extensions/ServiceCollectionExtensions.cs
index c9fe547..c46ad41 100644
--- a/src/Kentico.Xperience.Shopify/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Kentico.Xperience.Shopify/Extensions/ServiceCollectionExtensions.cs
@@ -53,6 +53,7 @@ public static void RegisterShopifyServices(this IServiceCollection services, ICo
// Add options monitor
services.Configure(configuration.GetSection(ShopifyConfig.SECTION_NAME));
+ services.Configure(configuration.GetSection(ShopifyWebsiteChannelConfigOptions.SECTION_NAME));
// Add HTTP session services
services.AddSession();
diff --git a/src/Kentico.Xperience.Shopify/Products/Models/ProductFilter.cs b/src/Kentico.Xperience.Shopify/Products/Models/ProductFilter.cs
index 902880f..b48c4d1 100644
--- a/src/Kentico.Xperience.Shopify/Products/Models/ProductFilter.cs
+++ b/src/Kentico.Xperience.Shopify/Products/Models/ProductFilter.cs
@@ -16,7 +16,7 @@ public class ProductFilter
///
/// The currency to use for pricing.
///
- public CurrencyCode? Currency { get; set; }
+ public CurrencyCode Currency { get; set; }
///
diff --git a/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs b/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs
index 664b36e..66dbc55 100644
--- a/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs
+++ b/src/Kentico.Xperience.Shopify/Products/ShopifyPriceService.cs
@@ -4,18 +4,22 @@
using ShopifySharp;
using ShopifySharp.Factories;
using ShopifySharp.Filters;
-using ShopifySharp.GraphQL;
namespace Kentico.Xperience.Shopify.Products;
internal class ShopifyPriceService : ShopifyServiceBase, IShopifyPriceService
{
private readonly IProductService productService;
+ private readonly IShopifyIntegrationSettingsService settingsService;
- public ShopifyPriceService(IShopifyIntegrationSettingsService integrationSettingsService, IProductServiceFactory productServiceFactory)
+ public ShopifyPriceService(
+ IShopifyIntegrationSettingsService integrationSettingsService,
+ IProductServiceFactory productServiceFactory,
+ IShopifyIntegrationSettingsService settingsService)
: base(integrationSettingsService)
{
productService = productServiceFactory.Create(shopifyCredentials);
+ this.settingsService = settingsService;
}
public async Task> GetProductsPrice(IEnumerable shopifyProductIds)
@@ -27,14 +31,15 @@ public async Task> GetProductsPrice(IEnum
private async Task> GetProductsPriceInternal(IEnumerable shopifyProductIds)
{
- string currency = CurrencyCode.CZK.ToString();
+ string currencyCode = settingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty;
+
var dict = new Dictionary();
var filter = new ProductListFilter()
{
Ids = shopifyProductIds.Select(long.Parse),
Fields = "Variants,Id",
- PresentmentCurrencies = [currency]
+ PresentmentCurrencies = [currencyCode]
};
var result = await productService.ListAsync(filter, true);
@@ -45,7 +50,7 @@ private async Task> GetProductsPriceInter
continue;
}
- var prices = product.Variants.Select(x => x.PresentmentPrices.FirstOrDefault(x => x.Price.CurrencyCode.Equals(currency, StringComparison.Ordinal)));
+ var prices = product.Variants.Select(x => x.PresentmentPrices.FirstOrDefault(x => x.Price.CurrencyCode.Equals(currencyCode, StringComparison.Ordinal)));
dict.TryAdd(product.Id.Value.ToString(), new ProductPriceModel()
{
diff --git a/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs b/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs
index b4cc668..f03297a 100644
--- a/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs
+++ b/src/Kentico.Xperience.Shopify/Products/ShopifyProductService.cs
@@ -15,6 +15,7 @@ internal class ShopifyProductService : ShopifyServiceBase, IShopifyProductServic
private readonly IProductService productService;
private readonly IShopifyInventoryService inventoryService;
private readonly IShoppingService shoppingService;
+ private readonly IShopifyIntegrationSettingsService settingsService;
private readonly Uri shopifyProductUrlBase;
private readonly string[] _shopifyFields = ["title", "body_html", "handle", "images", "variants"];
@@ -26,10 +27,12 @@ public ShopifyProductService(
IShopifyIntegrationSettingsService integrationSettingsService,
IProductServiceFactory productServiceFactory,
IShopifyInventoryService inventoryService,
- IShoppingService shoppingService) : base(integrationSettingsService)
+ IShoppingService shoppingService,
+ IShopifyIntegrationSettingsService settingsService) : base(integrationSettingsService)
{
this.inventoryService = inventoryService;
this.shoppingService = shoppingService;
+ this.settingsService = settingsService;
productService = productServiceFactory.Create(shopifyCredentials);
@@ -79,7 +82,7 @@ private async Task> GetProductsIntern
{
var filter = new ListFilter(filterParams?.PageInfo, filterParams?.Limit, ShopifyFields);
var result = await productService.ListAsync(filter, true);
- return CreateResultModel(result, "USD");
+ return CreateResultModel(result, settingsService.GetWebsiteChannelSettings()?.CurrencyCode ?? string.Empty);
}
private async Task> GetProductVariantsInternal(string shopifyProductID, string currencyCode)
@@ -106,7 +109,7 @@ private async Task> GetProductsAsyncI
CollectionId = initialFilter.CollectionID,
Limit = initialFilter.Limit,
Ids = initialFilter.Ids,
- PresentmentCurrencies = [initialFilter.Currency?.ToString() ?? string.Empty]
+ PresentmentCurrencies = [initialFilter.Currency.ToString()]
};
var result = await productService.ListAsync(filter, true);