mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 11:35:51 +01:00
Kukks Shopify Enhancement Suite Commit
This commit is contained in:
parent
1c510d45f0
commit
0cf9b20328
16 changed files with 509 additions and 266 deletions
176
BTCPayServer/Controllers/StoresController.Integrations.cs
Normal file
176
BTCPayServer/Controllers/StoresController.Integrations.cs
Normal file
|
@ -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<IActionResult> 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<IActionResult> 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});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IActionResult> Integrations()
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
|
||||
var vm = new IntegrationsViewModel
|
||||
{
|
||||
Shopify = blob.Shopify
|
||||
};
|
||||
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/integrations")]
|
||||
public async Task<IActionResult> 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
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -247,7 +247,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
|
||||
services.AddSingleton<IHostedService, ShopifyOrderMarkerHostedService>();
|
||||
services.AddShopify();
|
||||
#if DEBUG
|
||||
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
||||
#endif
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<object> Fields { get; set; }
|
||||
|
||||
[JsonProperty("metafield_namespaces")] public List<object> MetafieldNamespaces { get; set; }
|
||||
|
||||
[JsonProperty("api_version")] public string ApiVersion { get; set; }
|
||||
|
||||
[JsonProperty("private_metafield_namespaces")]
|
||||
public List<object> PrivateMetafieldNamespaces { get; set; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
25
BTCPayServer/Services/Shopify/Models/ShopifySettings.cs
Normal file
25
BTCPayServer/Services/Shopify/Models/ShopifySettings.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<string> sendRequest(HttpRequestMessage req)
|
||||
private async Task<string> SendRequest(HttpRequestMessage req)
|
||||
{
|
||||
using var resp = await _httpClient.SendAsync(req);
|
||||
|
||||
var strResp = await resp.Content.ReadAsStringAsync();
|
||||
return strResp;
|
||||
}
|
||||
|
||||
public async Task<CreateWebhookResponse> 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<CreateWebhookResponse>(strResp);
|
||||
}
|
||||
|
||||
public async Task RemoveWebhook(string id)
|
||||
{
|
||||
var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"webhooks/{id}.json");
|
||||
await SendRequest(req);
|
||||
}
|
||||
|
||||
public async Task<CreateScriptResponse> 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<CreateScriptResponse>(strResp);
|
||||
}
|
||||
|
||||
public async Task RemoveScript(string id)
|
||||
{
|
||||
var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"script_tags/{id}.json");
|
||||
await SendRequest(req);
|
||||
}
|
||||
|
||||
public async Task<TransactionsListResp> 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<TransactionsListResp>(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<TransactionsCreateResp>(strResp);
|
||||
}
|
||||
|
||||
public async Task<long> 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<OrdersCountResp>(strResp);
|
||||
var parsed = JsonConvert.DeserializeObject<CountResponse>(strResp);
|
||||
|
||||
return parsed.count;
|
||||
return parsed.Count;
|
||||
}
|
||||
|
||||
public async Task<bool> 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; }
|
||||
}
|
||||
}
|
||||
|
|
24
BTCPayServer/Services/Shopify/ShopifyExtensions.cs
Normal file
24
BTCPayServer/Services/Shopify/ShopifyExtensions.cs
Normal file
|
@ -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<IHostedService, ShopifyOrderMarkerHostedService>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<InvoiceEvent>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
leases.Add(_eventAggregator.Subscribe<Events.InvoiceEvent>(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
@ -22,60 +17,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<h4 class="mb-3">
|
||||
Shopify
|
||||
<a href="https://docs.btcpayserver.org/Shopify" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.ShopName"></label>
|
||||
<input asp-for="Shopify.ShopName" class="form-control" readonly="@shopifyCredsSet" />
|
||||
<span asp-validation-for="Shopify.ShopName" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.ApiKey"></label>
|
||||
<input asp-for="Shopify.ApiKey" class="form-control" readonly="@shopifyCredsSet" />
|
||||
<span asp-validation-for="Shopify.ApiKey" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.Password"></label>
|
||||
<input asp-for="Shopify.Password" class="form-control" type="password" value="@Model.Shopify?.Password" readonly="@shopifyCredsSet" />
|
||||
<span asp-validation-for="Shopify.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.SharedSecret"></label>
|
||||
<input asp-for="Shopify.SharedSecret" class="form-control" readonly="@shopifyCredsSet" />
|
||||
<span asp-validation-for="Shopify.SharedSecret" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if (!shopifyCredsSet)
|
||||
{
|
||||
<button name="command" type="submit" class="btn btn-primary" value="ShopifySaveCredentials">Save Credentials</button>
|
||||
}
|
||||
else if (shopify?.IntegratedAt.HasValue == true)
|
||||
{
|
||||
<p>
|
||||
Orders on <b>@shopify.ShopName</b>.myshopify.com will be marked as paid on successful invoice payment.
|
||||
Started: @shopify.IntegratedAt.Value.ToBrowserDate()
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (shopifyCredsSet)
|
||||
{
|
||||
if (!shopify.IntegratedAt.HasValue)
|
||||
{
|
||||
<button name="command" type="submit" class="btn btn-primary" value="ShopifyIntegrate">Integrate Shopify Order Paid Marking</button>
|
||||
}
|
||||
<button name="command" type="submit" class="btn btn-danger" value="ShopifyClearCredentials">
|
||||
@(shopify.IntegratedAt.HasValue? "Stop Shopify calls and clear credentials" : "Clear credentials")
|
||||
</button>
|
||||
}
|
||||
|
||||
</form>
|
||||
<partial name="Integrations/Shopify"/>
|
||||
|
||||
<h4 class="mb-3 mt-5">
|
||||
Other Integrations
|
||||
|
|
102
BTCPayServer/Views/Stores/Integrations/Shopify.cshtml
Normal file
102
BTCPayServer/Views/Stores/Integrations/Shopify.cshtml
Normal file
|
@ -0,0 +1,102 @@
|
|||
@model IntegrationsViewModel
|
||||
@{
|
||||
var shopify = Model.Shopify;
|
||||
var shopifyCredsSet = shopify?.CredentialsValid == true;
|
||||
}
|
||||
<script>
|
||||
function promptExampleUrl(){
|
||||
var exampleUrl = prompt("Enter Example URL from the private app");
|
||||
if (!exampleUrl)
|
||||
return;
|
||||
$("#exampleUrl").val(exampleUrl);
|
||||
$("#shopifyForm").submit();
|
||||
}
|
||||
</script>
|
||||
<form method="post" id="shopifyForm">
|
||||
<h4 class="mb-3">
|
||||
Shopify
|
||||
<a href="https://docs.btcpayserver.org/Shopify" target="_blank">
|
||||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
</h4>
|
||||
@if (!shopifyCredsSet)
|
||||
{
|
||||
<p class="alert alert-info">Create a Shopify private app with the permission "Script tags - Read and write" then <a href="#" class="alert-link" onclick="promptExampleUrl()">click here and paste the provided example URL.</a></p>
|
||||
<input type="hidden" id="exampleUrl" name="exampleUrl">
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.ShopName"></label>
|
||||
<div class="input-group">
|
||||
@if (!Model.Shopify?.ShopName?.Contains(".") is true)
|
||||
{
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">https://</span>
|
||||
</div>
|
||||
}
|
||||
<input asp-for="Shopify.ShopName" class="form-control" readonly="@shopifyCredsSet"/>
|
||||
|
||||
@if (!Model.Shopify?.ShopName?.Contains(".") is true)
|
||||
{
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">.myshopify.com</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<span asp-validation-for="Shopify.ShopName" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.ApiKey"></label>
|
||||
<input asp-for="Shopify.ApiKey" class="form-control" readonly="@shopifyCredsSet"/>
|
||||
<span asp-validation-for="Shopify.ApiKey" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Shopify.Password"></label>
|
||||
<input asp-for="Shopify.Password" class="form-control" type="password" value="@Model.Shopify?.Password" readonly="@shopifyCredsSet"/>
|
||||
<span asp-validation-for="Shopify.Password" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if (!shopifyCredsSet)
|
||||
{
|
||||
<button name="command" type="submit" class="btn btn-primary" value="ShopifySaveCredentials">Save Credentials</button>
|
||||
}
|
||||
else if (shopify?.IntegratedAt.HasValue == true)
|
||||
{
|
||||
<p class="alert alert-success">
|
||||
Orders on <b>@shopify.ShopName</b>.myshopify.com will be marked as paid on successful invoice payment.
|
||||
Started: @shopify.IntegratedAt.Value.ToBrowserDate()
|
||||
</p>
|
||||
@if (string.IsNullOrEmpty(shopify.ScriptId))
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Scripts could not automatically be added, please ensure the following is saved at
|
||||
<a class="alert-link" href="https://@(shopify.ShopName).myshopify.com/admin/settings/checkout#PolarisTextField1" target="_blank"> Settings > Checkout > Additional Scripts</a>
|
||||
</p>
|
||||
<code class="html">
|
||||
@($"<script src='{Url.Action("ShopifyJavascript", "Stores", new {storeId = Context.GetRouteValue("storeId")}, Context.Request.Scheme)}'></script>")
|
||||
</code>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Checkout scripts should be automatically added to the order status page to display the BTCPay payment option.</p>
|
||||
}
|
||||
<p class="alert alert-info">
|
||||
Please add a payment method at <a href="https://@(shopify.ShopName).myshopify.com/admin/settings/payments" target="_blank"> Settings > Payments > Manual Payment Methods</a> with the name <kbd>Bitcoin with BTCPay Server</kbd>
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (shopifyCredsSet)
|
||||
{
|
||||
if (!shopify.IntegratedAt.HasValue)
|
||||
{
|
||||
<button name="command" type="submit" class="btn btn-primary" value="ShopifyIntegrate">Integrate Shopify Order Paid Marking</button>
|
||||
}
|
||||
<button name="command" type="submit" class="btn btn-danger" value="ShopifyClearCredentials">
|
||||
@(shopify.IntegratedAt.HasValue ? "Stop Shopify calls and clear credentials" : "Clear credentials")
|
||||
</button>
|
||||
}
|
||||
|
||||
</form>
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue