mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Kukks Shopify Enhancement Suite Commit
This commit is contained in:
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.Shopify;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using BundlerMinifier.TagHelpers;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@@ -63,7 +64,9 @@ namespace BTCPayServer.Controllers
|
|||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
CssThemeManager cssThemeManager,
|
CssThemeManager cssThemeManager,
|
||||||
AppService appService)
|
AppService appService,
|
||||||
|
IWebHostEnvironment webHostEnvironment,
|
||||||
|
IBundleProvider bundleProvider)
|
||||||
{
|
{
|
||||||
_RateFactory = rateFactory;
|
_RateFactory = rateFactory;
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
@@ -79,6 +82,8 @@ namespace BTCPayServer.Controllers
|
|||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_CssThemeManager = cssThemeManager;
|
_CssThemeManager = cssThemeManager;
|
||||||
_appService = appService;
|
_appService = appService;
|
||||||
|
_webHostEnvironment = webHostEnvironment;
|
||||||
|
_bundleProvider = bundleProvider;
|
||||||
_EventAggregator = eventAggregator;
|
_EventAggregator = eventAggregator;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_ExplorerProvider = explorerProvider;
|
_ExplorerProvider = explorerProvider;
|
||||||
@@ -107,6 +112,8 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly CssThemeManager _CssThemeManager;
|
private readonly CssThemeManager _CssThemeManager;
|
||||||
private readonly AppService _appService;
|
private readonly AppService _appService;
|
||||||
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||||
|
private readonly IBundleProvider _bundleProvider;
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
|
|
||||||
[TempData]
|
[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.Rating;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Shopify.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
@@ -26,27 +27,7 @@ namespace BTCPayServer.Data
|
|||||||
RecommendedFeeBlockTarget = 1;
|
RecommendedFeeBlockTarget = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ShopifyDataHolder Shopify { get; set; }
|
public ShopifySettings 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("Use NetworkFeeMode instead")]
|
[Obsolete("Use NetworkFeeMode instead")]
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||||
|
|
||||||
services.AddSingleton<IHostedService, ShopifyOrderMarkerHostedService>();
|
services.AddShopify();
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Services.Shopify.Models;
|
||||||
using static BTCPayServer.Data.StoreBlob;
|
using static BTCPayServer.Data.StoreBlob;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
{
|
{
|
||||||
public class IntegrationsViewModel
|
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 Newtonsoft.Json;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Shopify.ApiModels
|
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
|
namespace BTCPayServer.Services.Shopify.ApiModels
|
||||||
{
|
{
|
||||||
public class TransactionsCreateReq
|
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;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Services.Shopify.ApiModels;
|
using BTCPayServer.Services.Shopify.ApiModels;
|
||||||
using DBriize.Utils;
|
using DBriize.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Shopify
|
namespace BTCPayServer.Services.Shopify
|
||||||
{
|
{
|
||||||
public class ShopifyApiClient
|
public class ShopifyApiClient
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
private readonly ShopifyApiClientCredentials _credentials;
|
||||||
private readonly ShopifyApiClientCredentials _creds;
|
|
||||||
|
|
||||||
public ShopifyApiClient(IHttpClientFactory httpClientFactory, ILogger logger, ShopifyApiClientCredentials creds)
|
public ShopifyApiClient(IHttpClientFactory httpClientFactory, ShopifyApiClientCredentials credentials)
|
||||||
{
|
{
|
||||||
if (httpClientFactory != null)
|
if (httpClientFactory != null)
|
||||||
{
|
{
|
||||||
@@ -28,37 +23,74 @@ namespace BTCPayServer.Services.Shopify
|
|||||||
{
|
{
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
}
|
}
|
||||||
_logger = logger;
|
_credentials = credentials;
|
||||||
_creds = creds;
|
|
||||||
|
|
||||||
var bearer = $"{creds.ApiKey}:{creds.ApiPassword}";
|
var bearer = $"{credentials.ApiKey}:{credentials.ApiPassword}";
|
||||||
bearer = Encoding.UTF8.GetBytes(bearer).ToBase64String();
|
bearer = Encoding.UTF8.GetBytes(bearer).ToBase64String();
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer);
|
_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);
|
var req = new HttpRequestMessage(method, url);
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> sendRequest(HttpRequestMessage req)
|
private async Task<string> SendRequest(HttpRequestMessage req)
|
||||||
{
|
{
|
||||||
using var resp = await _httpClient.SendAsync(req);
|
using var resp = await _httpClient.SendAsync(req);
|
||||||
|
|
||||||
var strResp = await resp.Content.ReadAsStringAsync();
|
var strResp = await resp.Content.ReadAsStringAsync();
|
||||||
return strResp;
|
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)
|
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);
|
var parsed = JsonConvert.DeserializeObject<TransactionsListResp>(strResp);
|
||||||
|
|
||||||
@@ -69,27 +101,27 @@ namespace BTCPayServer.Services.Shopify
|
|||||||
{
|
{
|
||||||
var postJson = JsonConvert.SerializeObject(txnCreate);
|
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");
|
req.Content = new StringContent(postJson, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var strResp = await sendRequest(req);
|
var strResp = await SendRequest(req);
|
||||||
return JsonConvert.DeserializeObject<TransactionsCreateResp>(strResp);
|
return JsonConvert.DeserializeObject<TransactionsCreateResp>(strResp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<long> OrdersCount()
|
public async Task<long> OrdersCount()
|
||||||
{
|
{
|
||||||
var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/count.json");
|
var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/count.json");
|
||||||
var strResp = await sendRequest(req);
|
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)
|
public async Task<bool> OrderExists(string orderId)
|
||||||
{
|
{
|
||||||
var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}.json?fields=id");
|
var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/{orderId}.json?fields=id");
|
||||||
var strResp = await sendRequest(req);
|
var strResp = await SendRequest(req);
|
||||||
|
|
||||||
return strResp?.Contains(orderId, StringComparison.OrdinalIgnoreCase) == true;
|
return strResp?.Contains(orderId, StringComparison.OrdinalIgnoreCase) == true;
|
||||||
}
|
}
|
||||||
@@ -100,6 +132,5 @@ namespace BTCPayServer.Services.Shopify
|
|||||||
public string ShopName { get; set; }
|
public string ShopName { get; set; }
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
public string ApiPassword { 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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Services.Shopify.Models;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBXplorer;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Shopify
|
namespace BTCPayServer.Services.Shopify
|
||||||
{
|
{
|
||||||
public class ShopifyOrderMarkerHostedService : IHostedService
|
public class ShopifyOrderMarkerHostedService : EventHostedServiceBase
|
||||||
{
|
{
|
||||||
private readonly EventAggregator _eventAggregator;
|
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
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;
|
_storeRepository = storeRepository;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource _Cts;
|
|
||||||
private readonly CompositeDisposable leases = new CompositeDisposable();
|
|
||||||
|
|
||||||
public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-";
|
public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-";
|
||||||
|
|
||||||
|
protected override void SubscribeToEvents()
|
||||||
private static readonly SemaphoreSlim _shopifyEventsSemaphore = new SemaphoreSlim(1, 1);
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
_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;
|
var shopifyOrderId = invoice.Metadata?.OrderId;
|
||||||
// We're only registering transaction on confirmed or complete and if invoice has 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)
|
&& shopifyOrderId != null)
|
||||||
{
|
{
|
||||||
var storeData = await _storeRepository.FindStore(invoice.StoreId);
|
var storeData = await _storeRepository.FindStore(invoice.StoreId);
|
||||||
@@ -53,11 +53,9 @@ namespace BTCPayServer.Services.Shopify
|
|||||||
if (storeBlob.Shopify?.IntegratedAt.HasValue == true &&
|
if (storeBlob.Shopify?.IntegratedAt.HasValue == true &&
|
||||||
shopifyOrderId.StartsWith(SHOPIFY_ORDER_ID_PREFIX, StringComparison.OrdinalIgnoreCase))
|
shopifyOrderId.StartsWith(SHOPIFY_ORDER_ID_PREFIX, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await _shopifyEventsSemaphore.WaitAsync();
|
|
||||||
|
|
||||||
shopifyOrderId = shopifyOrderId[SHOPIFY_ORDER_ID_PREFIX.Length..];
|
shopifyOrderId = shopifyOrderId[SHOPIFY_ORDER_ID_PREFIX.Length..];
|
||||||
|
|
||||||
var client = createShopifyApiClient(storeBlob.Shopify);
|
var client = CreateShopifyApiClient(storeBlob.Shopify);
|
||||||
if (!await client.OrderExists(shopifyOrderId))
|
if (!await client.OrderExists(shopifyOrderId))
|
||||||
{
|
{
|
||||||
// don't register transactions for orders that don't exist on shopify
|
// 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
|
// OrderTransactionRegisterLogic has check if transaction is already registered which is why we're passing invoice.Id
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _shopifyEventsSemaphore.WaitAsync();
|
|
||||||
|
|
||||||
var logic = new OrderTransactionRegisterLogic(client);
|
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)
|
if (resp != null)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogInformation("Registered order transaction on Shopify. " +
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogError(ex, $"Shopify error while trying to register order transaction. " +
|
Logs.PayServer.LogError(ex, $"Shopify error while trying to register order transaction. " +
|
||||||
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}");
|
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}");
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_shopifyEventsSemaphore.Release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}
|
||||||
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();
|
return new ShopifyApiClient(_httpClientFactory, shopify.CreateShopifyApiCredentials());
|
||||||
leases.Dispose();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
@using static BTCPayServer.Data.StoreBlob
|
|
||||||
@model IntegrationsViewModel
|
@model IntegrationsViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData.SetActivePageAndTitle(StoreNavPages.Integrations, "Integrations");
|
ViewData.SetActivePageAndTitle(StoreNavPages.Integrations, "Integrations");
|
||||||
|
|
||||||
|
|
||||||
var shopify = Model.Shopify;
|
|
||||||
var shopifyCredsSet = shopify?.CredentialsValid == true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<partial name="_StatusMessage" />
|
<partial name="_StatusMessage" />
|
||||||
@@ -22,60 +17,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<form method="post">
|
<partial name="Integrations/Shopify"/>
|
||||||
<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>
|
|
||||||
|
|
||||||
<h4 class="mb-3 mt-5">
|
<h4 class="mb-3 mt-5">
|
||||||
Other Integrations
|
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.setApiUrlPrefix(btcPayServerUrl);
|
||||||
window.btcpay.onModalWillEnter(function () {
|
window.btcpay.onModalWillEnter(function () {
|
||||||
var interval = setInterval(function () {
|
var stopLoop = false;
|
||||||
|
function loopCheck(){
|
||||||
|
if(stopLoop){
|
||||||
|
return;
|
||||||
|
}
|
||||||
getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId)
|
getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId)
|
||||||
.then(function (invoice) {
|
.then(function (invoice) {
|
||||||
// in most cases this will be triggered by paid, but we put other statuses just in case
|
// 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) {
|
.catch(function (err) {
|
||||||
clearInterval(interval);
|
stopLoop = true;
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
}).finally(function(){
|
||||||
}, 1000);
|
if(!stopLoop){
|
||||||
|
setTimeout(loopCheck, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loopCheck();
|
||||||
window.btcpay.onModalWillLeave(function () {
|
window.btcpay.onModalWillLeave(function () {
|
||||||
waitForPayment.lock = false;
|
waitForPayment.lock = false;
|
||||||
clearInterval(interval);
|
stopLoop = true;
|
||||||
// If user exited the payment modal,
|
// If user exited the payment modal,
|
||||||
// indicate that there was no error but invoice did not complete.
|
// indicate that there was no error but invoice did not complete.
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user