diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs new file mode 100644 index 000000000..f13b0da0a --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -0,0 +1,176 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Services.Shopify; +using BTCPayServer.Services.Shopify.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + [AllowAnonymous] + [HttpGet("{storeId}/integrations/shopify/shopify.js")] + public async Task ShopifyJavascript(string storeId) + { + + string[] fileList = new[] + { + "modal/btcpay.js", + "shopify/btcpay-browser-client.js", + "shopify/btcpay-shopify-checkout.js" + }; + if (_BtcpayServerOptions.BundleJsCss) + { + fileList = new[] {_bundleProvider.GetBundle("shopify-bundle.min.js").OutputFileUrl}; + } + + var jsFile = $"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\";"; + foreach (var file in fileList) + { + await using var stream = _webHostEnvironment.WebRootFileProvider + .GetFileInfo(file).CreateReadStream(); + using var reader = new StreamReader(stream); + jsFile += Environment.NewLine + await reader.ReadToEndAsync(); + } + + return Content(jsFile, "text/javascript"); + } + + [HttpGet] + [Route("{storeId}/integrations")] + [Route("{storeId}/integrations/shopify")] + public IActionResult Integrations() + { + var blob = CurrentStore.GetStoreBlob(); + + var vm = new IntegrationsViewModel {Shopify = blob.Shopify}; + + return View("Integrations", vm); + } + + [HttpPost] + [Route("{storeId}/integrations/shopify")] + public async Task Integrations([FromServices] IHttpClientFactory clientFactory, + IntegrationsViewModel vm, string command = "", string exampleUrl = "") + { + if (!string.IsNullOrEmpty(exampleUrl)) + { + try + { +//https://{apikey}:{password}@{hostname}/admin/api/{version}/{resource}.json + var parsedUrl = new Uri(exampleUrl); + var userInfo = parsedUrl.UserInfo.Split(":"); + vm.Shopify = new ShopifySettings() + { + ApiKey = userInfo[0], + Password = userInfo[1], + ShopName = parsedUrl.Host.Replace(".myshopify.com", "", StringComparison.InvariantCultureIgnoreCase) + }; + command = "ShopifySaveCredentials"; + + } + catch (Exception) + { + TempData[WellKnownTempData.ErrorMessage] = "The provided example url was invalid."; + return View("Integrations", vm); + } + } + switch (command) + { + case "ShopifySaveCredentials": + { + var shopify = vm.Shopify; + var validCreds = shopify != null && shopify?.CredentialsPopulated() == true; + if (!validCreds) + { + TempData[WellKnownTempData.ErrorMessage] = "Please provide valid Shopify credentials"; + return View("Integrations", vm); + } + + var apiClient = new ShopifyApiClient(clientFactory, shopify.CreateShopifyApiCredentials()); + try + { + await apiClient.OrdersCount(); + } + catch + { + TempData[WellKnownTempData.ErrorMessage] = + "Shopify rejected provided credentials, please correct values and again"; + return View("Integrations", vm); + } + + shopify.CredentialsValid = true; + + var blob = CurrentStore.GetStoreBlob(); + blob.Shopify = shopify; + if (CurrentStore.SetStoreBlob(blob)) + { + await _Repo.UpdateStore(CurrentStore); + } + + TempData[WellKnownTempData.SuccessMessage] = "Shopify credentials successfully updated"; + break; + } + case "ShopifyIntegrate": + { + var blob = CurrentStore.GetStoreBlob(); + + var apiClient = new ShopifyApiClient(clientFactory, blob.Shopify.CreateShopifyApiCredentials()); + var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", + new {storeId = CurrentStore.Id}, Request.Scheme)); + + blob.Shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); + + blob.Shopify.IntegratedAt = DateTimeOffset.UtcNow; + if (CurrentStore.SetStoreBlob(blob)) + { + await _Repo.UpdateStore(CurrentStore); + } + + TempData[WellKnownTempData.SuccessMessage] = "Shopify integration successfully turned on"; + break; + } + case "ShopifyClearCredentials": + { + var blob = CurrentStore.GetStoreBlob(); + + if (blob.Shopify.IntegratedAt.HasValue) + { + if (!string.IsNullOrEmpty(blob.Shopify.ScriptId)) + { + try + { + var apiClient = new ShopifyApiClient(clientFactory, + blob.Shopify.CreateShopifyApiCredentials()); + await apiClient.RemoveScript(blob.Shopify.ScriptId); + } + catch (Exception e) + { + //couldnt remove the script but that's ok + } + } + } + + blob.Shopify = null; + if (CurrentStore.SetStoreBlob(blob)) + { + await _Repo.UpdateStore(CurrentStore); + } + + TempData[WellKnownTempData.SuccessMessage] = "Shopify integration credentials cleared"; + break; + } + } + + return RedirectToAction(nameof(Integrations), new {storeId = CurrentStore.Id}); + } + } +} diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 132c036e8..237529b3a 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -23,6 +23,7 @@ using BTCPayServer.Services.Rates; using BTCPayServer.Services.Shopify; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; +using BundlerMinifier.TagHelpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -63,7 +64,9 @@ namespace BTCPayServer.Controllers IAuthorizationService authorizationService, EventAggregator eventAggregator, CssThemeManager cssThemeManager, - AppService appService) + AppService appService, + IWebHostEnvironment webHostEnvironment, + IBundleProvider bundleProvider) { _RateFactory = rateFactory; _Repo = repo; @@ -79,6 +82,8 @@ namespace BTCPayServer.Controllers _authorizationService = authorizationService; _CssThemeManager = cssThemeManager; _appService = appService; + _webHostEnvironment = webHostEnvironment; + _bundleProvider = bundleProvider; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; @@ -107,6 +112,8 @@ namespace BTCPayServer.Controllers private readonly IAuthorizationService _authorizationService; private readonly CssThemeManager _CssThemeManager; private readonly AppService _appService; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly IBundleProvider _bundleProvider; private readonly EventAggregator _EventAggregator; [TempData] @@ -966,99 +973,6 @@ namespace BTCPayServer.Controllers } - // - [HttpGet] - [Route("{storeId}/integrations")] - public async Task Integrations() - { - var blob = CurrentStore.GetStoreBlob(); - - var vm = new IntegrationsViewModel - { - Shopify = blob.Shopify - }; - - return View("Integrations", vm); - } - - [HttpPost] - [Route("{storeId}/integrations")] - public async Task Integrations([FromServices] IHttpClientFactory clientFactory, - IntegrationsViewModel vm, string command = "") - { - if (command == "ShopifySaveCredentials") - { - var shopify = vm.Shopify; - var validCreds = shopify != null && shopify?.CredentialsPopulated() == true; - if (!validCreds) - { - TempData[WellKnownTempData.ErrorMessage] = "Please provide valid Shopify credentials"; - // - return View("Integrations", vm); - } - - var apiCreds = new ShopifyApiClientCredentials - { - ShopName = shopify.ShopName, - ApiKey = shopify.ApiKey, - ApiPassword = shopify.Password, - SharedSecret = shopify.SharedSecret - }; - - var apiClient = new ShopifyApiClient(clientFactory, null, apiCreds); - try - { - var result = await apiClient.OrdersCount(); - } - catch - { - TempData[WellKnownTempData.ErrorMessage] = "Shopify rejected provided credentials, please correct values and again"; - // - return View("Integrations", vm); - } - - - shopify.CredentialsValid = true; - - var blob = CurrentStore.GetStoreBlob(); - blob.Shopify = shopify; - if (CurrentStore.SetStoreBlob(blob)) - { - await _Repo.UpdateStore(CurrentStore); - } - TempData[WellKnownTempData.SuccessMessage] = "Shopify credentials successfully updated"; - } - else if (command == "ShopifyIntegrate") - { - var shopify = vm.Shopify; - - var blob = CurrentStore.GetStoreBlob(); - blob.Shopify.IntegratedAt = DateTimeOffset.UtcNow; - if (CurrentStore.SetStoreBlob(blob)) - { - await _Repo.UpdateStore(CurrentStore); - } - TempData[WellKnownTempData.SuccessMessage] = "Shopify integration successfully turned on"; - } - else if (command == "ShopifyClearCredentials") - { - var shopify = vm.Shopify; - - var blob = CurrentStore.GetStoreBlob(); - blob.Shopify = null; - if (CurrentStore.SetStoreBlob(blob)) - { - await _Repo.UpdateStore(CurrentStore); - } - TempData[WellKnownTempData.SuccessMessage] = "Shopify integration credentials cleared"; - } - - return RedirectToAction(nameof(Integrations), new - { - storeId = CurrentStore.Id - }); - - } } diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index 80abb45f4..e0e8ffc1a 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -11,6 +11,7 @@ using BTCPayServer.Payments.CoinSwitch; using BTCPayServer.Rating; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Shopify.Models; using Newtonsoft.Json; namespace BTCPayServer.Data @@ -26,27 +27,7 @@ namespace BTCPayServer.Data RecommendedFeeBlockTarget = 1; } - public ShopifyDataHolder Shopify { get; set; } - - public class ShopifyDataHolder - { - public string ShopName { get; set; } - public string ApiKey { get; set; } - public string Password { get; set; } - public string SharedSecret { get; set; } - - public bool CredentialsPopulated() - { - return - !String.IsNullOrWhiteSpace(ShopName) && - !String.IsNullOrWhiteSpace(ApiKey) && - !String.IsNullOrWhiteSpace(Password) && - !String.IsNullOrWhiteSpace(SharedSecret); - } - public bool CredentialsValid { get; set; } - - public DateTimeOffset? IntegratedAt { get; set; } - } + public ShopifySettings Shopify { get; set; } [Obsolete("Use NetworkFeeMode instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index fa55e1d19..d391911ae 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -247,7 +247,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddShopify(); #if DEBUG services.AddSingleton(); #endif diff --git a/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs b/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs index 765049f13..d5b6d3b89 100644 --- a/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs @@ -3,12 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Services.Shopify.Models; using static BTCPayServer.Data.StoreBlob; namespace BTCPayServer.Models.StoreViewModels { public class IntegrationsViewModel { - public ShopifyDataHolder Shopify { get; set; } + public ShopifySettings Shopify { get; set; } } } diff --git a/BTCPayServer/Services/Shopify/ApiModels/CreateScriptResponse.cs b/BTCPayServer/Services/Shopify/ApiModels/CreateScriptResponse.cs new file mode 100644 index 000000000..d5560abd5 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/CreateScriptResponse.cs @@ -0,0 +1,31 @@ +using System; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class CreateScriptResponse + { + [JsonProperty("script_tag")] + public ScriptTag ScriptTag { get; set; } + } + + public class ScriptTag { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("src")] + public string Src { get; set; } + + [JsonProperty("event")] + public string Event { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonProperty("display_scope")] + public string DisplayScope { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/CreateWebhookResponse.cs b/BTCPayServer/Services/Shopify/ApiModels/CreateWebhookResponse.cs new file mode 100644 index 000000000..d4b7cc6e6 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/CreateWebhookResponse.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class CreateWebhookResponse + { + [JsonProperty("webhook")] public Webhook Webhook { get; set; } + } + + public class Webhook + { + [JsonProperty("id")] public int Id { get; set; } + + [JsonProperty("address")] public string Address { get; set; } + + [JsonProperty("topic")] public string Topic { get; set; } + + [JsonProperty("created_at")] public DateTime CreatedAt { get; set; } + + [JsonProperty("updated_at")] public DateTime UpdatedAt { get; set; } + + [JsonProperty("format")] public string Format { get; set; } + + [JsonProperty("fields")] public List Fields { get; set; } + + [JsonProperty("metafield_namespaces")] public List MetafieldNamespaces { get; set; } + + [JsonProperty("api_version")] public string ApiVersion { get; set; } + + [JsonProperty("private_metafield_namespaces")] + public List PrivateMetafieldNamespaces { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs b/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs index b34ad4e7c..7aef830b9 100644 --- a/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs +++ b/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Newtonsoft.Json; namespace BTCPayServer.Services.Shopify.ApiModels { - public class OrdersCountResp + public class CountResponse { - public long count { get; set; } + [JsonProperty("count")] + public long Count { get; set; } } } diff --git a/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs index 3720b81ea..0fc0e5370 100644 --- a/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs +++ b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace BTCPayServer.Services.Shopify.ApiModels { public class TransactionsCreateReq diff --git a/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs new file mode 100644 index 000000000..e7216afcf --- /dev/null +++ b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Services.Shopify.Models +{ + public class ShopifySettings + { + [Display(Name = "Shop Name")] + public string ShopName { get; set; } + public string ApiKey { get; set; } + public string Password { get; set; } + + public bool CredentialsPopulated() + { + return + !string.IsNullOrWhiteSpace(ShopName) && + !string.IsNullOrWhiteSpace(ApiKey) && + !string.IsNullOrWhiteSpace(Password); + } + + public bool CredentialsValid { get; set; } + public DateTimeOffset? IntegratedAt { get; set; } + public string ScriptId { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index 8142db419..cac85a635 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -1,24 +1,19 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using BTCPayServer.Services.Shopify.ApiModels; using DBriize.Utils; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Shopify { public class ShopifyApiClient { private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly ShopifyApiClientCredentials _creds; + private readonly ShopifyApiClientCredentials _credentials; - public ShopifyApiClient(IHttpClientFactory httpClientFactory, ILogger logger, ShopifyApiClientCredentials creds) + public ShopifyApiClient(IHttpClientFactory httpClientFactory, ShopifyApiClientCredentials credentials) { if (httpClientFactory != null) { @@ -28,37 +23,74 @@ namespace BTCPayServer.Services.Shopify { _httpClient = new HttpClient(); } - _logger = logger; - _creds = creds; + _credentials = credentials; - var bearer = $"{creds.ApiKey}:{creds.ApiPassword}"; + var bearer = $"{credentials.ApiKey}:{credentials.ApiPassword}"; bearer = Encoding.UTF8.GetBytes(bearer).ToBase64String(); _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer); } - private HttpRequestMessage createRequest(string shopNameInUrl, HttpMethod method, string action) + private HttpRequestMessage CreateRequest(string shopName, HttpMethod method, string action) { - var url = $"https://{shopNameInUrl}.myshopify.com/admin/api/2020-07/" + action; - + var url = $"https://{(shopName.Contains(".", StringComparison.InvariantCulture)? shopName: $"{shopName}.myshopify.com")}/admin/api/2020-07/" + action; var req = new HttpRequestMessage(method, url); - return req; } - private async Task sendRequest(HttpRequestMessage req) + private async Task SendRequest(HttpRequestMessage req) { using var resp = await _httpClient.SendAsync(req); var strResp = await resp.Content.ReadAsStringAsync(); return strResp; } + + public async Task CreateWebhook(string topic, string address, string format = "json") + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Post, $"webhooks.json"); + req.Content = new StringContent(JsonConvert.SerializeObject(new + { + topic, + address, + format + }), Encoding.UTF8, "application/json"); + var strResp = await SendRequest(req); + + return JsonConvert.DeserializeObject(strResp); + } + + public async Task RemoveWebhook(string id) + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"webhooks/{id}.json"); + await SendRequest(req); + } + + public async Task CreateScript(string scriptUrl, string evt = "onload", string scope = "order_status") + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Post, $"script_tags.json"); + req.Content = new StringContent(JsonConvert.SerializeObject(new + { + @event = evt, + src = scriptUrl, + display_scope = scope + }), Encoding.UTF8, "application/json"); + var strResp = await SendRequest(req); + + return JsonConvert.DeserializeObject(strResp); + } + + public async Task RemoveScript(string id) + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"script_tags/{id}.json"); + await SendRequest(req); + } public async Task TransactionsList(string orderId) { - var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); + var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); - var strResp = await sendRequest(req); + var strResp = await SendRequest(req); var parsed = JsonConvert.DeserializeObject(strResp); @@ -69,27 +101,27 @@ namespace BTCPayServer.Services.Shopify { var postJson = JsonConvert.SerializeObject(txnCreate); - var req = createRequest(_creds.ShopName, HttpMethod.Post, $"orders/{orderId}/transactions.json"); + var req = CreateRequest(_credentials.ShopName, HttpMethod.Post, $"orders/{orderId}/transactions.json"); req.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); - var strResp = await sendRequest(req); + var strResp = await SendRequest(req); return JsonConvert.DeserializeObject(strResp); } public async Task OrdersCount() { - var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/count.json"); - var strResp = await sendRequest(req); + var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/count.json"); + var strResp = await SendRequest(req); - var parsed = JsonConvert.DeserializeObject(strResp); + var parsed = JsonConvert.DeserializeObject(strResp); - return parsed.count; + return parsed.Count; } public async Task OrderExists(string orderId) { - var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}.json?fields=id"); - var strResp = await sendRequest(req); + var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/{orderId}.json?fields=id"); + var strResp = await SendRequest(req); return strResp?.Contains(orderId, StringComparison.OrdinalIgnoreCase) == true; } @@ -100,6 +132,5 @@ namespace BTCPayServer.Services.Shopify public string ShopName { get; set; } public string ApiKey { get; set; } public string ApiPassword { get; set; } - public string SharedSecret { get; set; } } } diff --git a/BTCPayServer/Services/Shopify/ShopifyExtensions.cs b/BTCPayServer/Services/Shopify/ShopifyExtensions.cs new file mode 100644 index 000000000..a92aa524c --- /dev/null +++ b/BTCPayServer/Services/Shopify/ShopifyExtensions.cs @@ -0,0 +1,24 @@ +using BTCPayServer.Services.Shopify.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace BTCPayServer.Services.Shopify +{ + public static class ShopifyExtensions + { + public static ShopifyApiClientCredentials CreateShopifyApiCredentials(this ShopifySettings shopify) + { + return new ShopifyApiClientCredentials + { + ShopName = shopify.ShopName, + ApiKey = shopify.ApiKey, + ApiPassword = shopify.Password + }; + } + + public static void AddShopify(this IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 36023ced3..99a9506df 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -4,45 +4,45 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.HostedServices; using BTCPayServer.Logging; +using BTCPayServer.Services.Shopify.Models; using BTCPayServer.Services.Stores; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using NBXplorer; namespace BTCPayServer.Services.Shopify { - public class ShopifyOrderMarkerHostedService : IHostedService + public class ShopifyOrderMarkerHostedService : EventHostedServiceBase { - private readonly EventAggregator _eventAggregator; private readonly StoreRepository _storeRepository; private readonly IHttpClientFactory _httpClientFactory; - public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator, StoreRepository storeRepository, IHttpClientFactory httpClientFactory) + public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator, + StoreRepository storeRepository, + IHttpClientFactory httpClientFactory) : base(eventAggregator) { - _eventAggregator = eventAggregator; _storeRepository = storeRepository; _httpClientFactory = httpClientFactory; } - private CancellationTokenSource _Cts; - private readonly CompositeDisposable leases = new CompositeDisposable(); - public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-"; - - private static readonly SemaphoreSlim _shopifyEventsSemaphore = new SemaphoreSlim(1, 1); - - public Task StartAsync(CancellationToken cancellationToken) + protected override void SubscribeToEvents() { - _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + Subscribe(); + base.SubscribeToEvents(); + } - leases.Add(_eventAggregator.Subscribe(async b => + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is InvoiceEvent invoiceEvent) { - var invoice = b.Invoice; + var invoice = invoiceEvent.Invoice; var shopifyOrderId = invoice.Metadata?.OrderId; // We're only registering transaction on confirmed or complete and if invoice has orderId - if ((invoice.Status == Client.Models.InvoiceStatus.Confirmed || invoice.Status == Client.Models.InvoiceStatus.Complete) + if ((invoice.Status == Client.Models.InvoiceStatus.Confirmed || + invoice.Status == Client.Models.InvoiceStatus.Complete) && shopifyOrderId != null) { var storeData = await _storeRepository.FindStore(invoice.StoreId); @@ -53,11 +53,9 @@ namespace BTCPayServer.Services.Shopify if (storeBlob.Shopify?.IntegratedAt.HasValue == true && shopifyOrderId.StartsWith(SHOPIFY_ORDER_ID_PREFIX, StringComparison.OrdinalIgnoreCase)) { - await _shopifyEventsSemaphore.WaitAsync(); - shopifyOrderId = shopifyOrderId[SHOPIFY_ORDER_ID_PREFIX.Length..]; - var client = createShopifyApiClient(storeBlob.Shopify); + var client = CreateShopifyApiClient(storeBlob.Shopify); if (!await client.OrderExists(shopifyOrderId)) { // don't register transactions for orders that don't exist on shopify @@ -68,48 +66,31 @@ namespace BTCPayServer.Services.Shopify // OrderTransactionRegisterLogic has check if transaction is already registered which is why we're passing invoice.Id try { - await _shopifyEventsSemaphore.WaitAsync(); - var logic = new OrderTransactionRegisterLogic(client); - var resp = await logic.Process(shopifyOrderId, invoice.Id, invoice.Currency, invoice.Price.ToString(CultureInfo.InvariantCulture)); + var resp = await logic.Process(shopifyOrderId, invoice.Id, invoice.Currency, + invoice.Price.ToString(CultureInfo.InvariantCulture)); if (resp != null) { Logs.PayServer.LogInformation("Registered order transaction on Shopify. " + - $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); } } catch (Exception ex) { Logs.PayServer.LogError(ex, $"Shopify error while trying to register order transaction. " + - $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); - } - finally - { - _shopifyEventsSemaphore.Release(); + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); } } } - })); - return Task.CompletedTask; + } + + await base.ProcessEvent(evt, cancellationToken); } - private ShopifyApiClient createShopifyApiClient(StoreBlob.ShopifyDataHolder shopify) - { - return new ShopifyApiClient(_httpClientFactory, null, new ShopifyApiClientCredentials - { - ShopName = shopify.ShopName, - ApiKey = shopify.ApiKey, - ApiPassword = shopify.Password, - SharedSecret = shopify.SharedSecret - }); - } - public Task StopAsync(CancellationToken cancellationToken) + private ShopifyApiClient CreateShopifyApiClient(ShopifySettings shopify) { - _Cts?.Cancel(); - leases.Dispose(); - - return Task.CompletedTask; + return new ShopifyApiClient(_httpClientFactory, shopify.CreateShopifyApiCredentials()); } } } diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index 11fd2400b..769fbc365 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -1,12 +1,7 @@ -@using static BTCPayServer.Data.StoreBlob @model IntegrationsViewModel @{ Layout = "../Shared/_NavLayout.cshtml"; ViewData.SetActivePageAndTitle(StoreNavPages.Integrations, "Integrations"); - - - var shopify = Model.Shopify; - var shopifyCredsSet = shopify?.CredentialsValid == true; } @@ -22,60 +17,7 @@
-
-

- Shopify - -

- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - @if (!shopifyCredsSet) - { - - } - else if (shopify?.IntegratedAt.HasValue == true) - { -

- Orders on @shopify.ShopName.myshopify.com will be marked as paid on successful invoice payment. - Started: @shopify.IntegratedAt.Value.ToBrowserDate() -

- } - - @if (shopifyCredsSet) - { - if (!shopify.IntegratedAt.HasValue) - { - - } - - } - -
+

Other Integrations diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml new file mode 100644 index 000000000..b8b85c213 --- /dev/null +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -0,0 +1,102 @@ +@model IntegrationsViewModel +@{ + var shopify = Model.Shopify; + var shopifyCredsSet = shopify?.CredentialsValid == true; +} + +
+

+ Shopify + + + +

+ @if (!shopifyCredsSet) + { +

Create a Shopify private app with the permission "Script tags - Read and write" then click here and paste the provided example URL.

+ + } +
+ +
+ @if (!Model.Shopify?.ShopName?.Contains(".") is true) + { +
+ https:// +
+ } + + + @if (!Model.Shopify?.ShopName?.Contains(".") is true) + { +
+ .myshopify.com +
+ } +
+ +
+ +
+ + + +
+ +
+ + + +
+ + @if (!shopifyCredsSet) + { + + } + else if (shopify?.IntegratedAt.HasValue == true) + { +

+ Orders on @shopify.ShopName.myshopify.com will be marked as paid on successful invoice payment. + Started: @shopify.IntegratedAt.Value.ToBrowserDate() +

+ @if (string.IsNullOrEmpty(shopify.ScriptId)) + { +
+

+ Scripts could not automatically be added, please ensure the following is saved at + Settings > Checkout > Additional Scripts +

+ + @($"") + +
+ } + else + { +

Checkout scripts should be automatically added to the order status page to display the BTCPay payment option.

+ } +

+ Please add a payment method at Settings > Payments > Manual Payment Methods with the name Bitcoin with BTCPay Server +

+ } + + @if (shopifyCredsSet) + { + if (!shopify.IntegratedAt.HasValue) + { + + } + + } + +
diff --git a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js index 77d2978ff..6db8f9265 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js @@ -18,7 +18,11 @@ var BtcPayServerModal = (function () { } window.btcpay.setApiUrlPrefix(btcPayServerUrl); window.btcpay.onModalWillEnter(function () { - var interval = setInterval(function () { + var stopLoop = false; + function loopCheck(){ + if(stopLoop){ + return; + } getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId) .then(function (invoice) { // in most cases this will be triggered by paid, but we put other statuses just in case @@ -28,13 +32,18 @@ var BtcPayServerModal = (function () { } }) .catch(function (err) { - clearInterval(interval); + stopLoop = true; reject(err); - }); - }, 1000); + }).finally(function(){ + if(!stopLoop){ + setTimeout(loopCheck, 1000); + } + }); + } + loopCheck(); window.btcpay.onModalWillLeave(function () { waitForPayment.lock = false; - clearInterval(interval); + stopLoop = true; // If user exited the payment modal, // indicate that there was no error but invoice did not complete. resolve(null);