From cf99f0fca0cf6a12ce06e7dfef4d6f09065985a2 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:07:55 -0500 Subject: [PATCH 01/63] Importing ShopifyApiClient for interactions with Shopify Api --- .../Services/Shopify/ShopifyApiClient.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 BTCPayServer/Services/Shopify/ShopifyApiClient.cs diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs new file mode 100644 index 000000000..dad345b7c --- /dev/null +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +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; + + public ShopifyApiClient(IHttpClientFactory httpClientFactory, ILogger logger, ShopifyApiClientCredentials creds) + { + if (httpClientFactory != null) + { + _httpClient = httpClientFactory.CreateClient(nameof(ShopifyApiClient)); + } + else // tests don't provide IHttpClientFactory + { + _httpClient = new HttpClient(); + } + _logger = logger; + _creds = creds; + + var bearer = $"{creds.ApiKey}:{creds.ApiPassword}"; + bearer = Encoding.UTF8.GetBytes(bearer).ToBase64String(); + + _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer); + } + + public async Task TransactionsList(string orderId) + { + var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); + + var strResp = await sendRequest(req); + + dynamic parsed = JObject.Parse(strResp); + + return parsed; + } + + public async Task TransactionCreate(string orderId, TransactionCreate txnCreate) + { + var postJson = JsonConvert.SerializeObject(txnCreate); + + var req = createRequest(_creds.ShopName, HttpMethod.Post, $"orders/{orderId}/transactions.json"); + req.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); + + var strResp = await sendRequest(req); + return JObject.Parse(strResp); + } + + private HttpRequestMessage createRequest(string shopNameInUrl, HttpMethod method, string action) + { + var url = $"https://{shopNameInUrl}.myshopify.com/admin/api/2020-07/" + action; + + var req = new HttpRequestMessage(method, url); + + return req; + } + + private async Task sendRequest(HttpRequestMessage req) + { + using var resp = await _httpClient.SendAsync(req); + + var strResp = await resp.Content.ReadAsStringAsync(); + return strResp; + } + } + + public class ShopifyApiClientCredentials + { + public string ShopName { get; set; } + public string ApiKey { get; set; } + public string ApiPassword { get; set; } + public string SharedSecret { get; set; } + } + + public class TransactionCreate + { + public DataHolder transaction { get; set; } + + public class DataHolder + { + public string currency { get; set; } + public string amount { get; set; } + public string kind { get; set; } + public string parent_id { get; set; } + public string gateway { get; set; } + public string source { get; set; } + } + } +} From 8a68e1b49d749fa0958f4b58a53d1120a88e9172 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:12:42 -0500 Subject: [PATCH 02/63] Importing UI for registering and verifying Shopify credentials --- BTCPayServer/Controllers/StoresController.cs | 85 ++++++++++++++++++ BTCPayServer/Data/StoreBlob.cs | 22 +++++ .../StoreViewModels/IntegrationsViewModel.cs | 14 +++ .../Services/Shopify/ShopifyApiClient.cs | 34 +++++--- BTCPayServer/Views/Stores/Integrations.cshtml | 87 +++++++++++++++++++ 5 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs create mode 100644 BTCPayServer/Views/Stores/Integrations.cshtml diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index dda4feff4..593bd324f 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -20,6 +20,7 @@ using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Shopify; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; @@ -964,5 +965,89 @@ 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"; + } + + return RedirectToAction(nameof(Integrations), new + { + storeId = CurrentStore.Id + }); + + } + + } } diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index 7149a2c71..80abb45f4 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -26,6 +26,28 @@ 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; } + } + [Obsolete("Use NetworkFeeMode instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool? NetworkFeeDisabled diff --git a/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs b/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs new file mode 100644 index 000000000..765049f13 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/IntegrationsViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static BTCPayServer.Data.StoreBlob; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class IntegrationsViewModel + { + public ShopifyDataHolder Shopify { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index dad345b7c..9cc5f66b8 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -36,6 +36,23 @@ namespace BTCPayServer.Services.Shopify _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer); } + private HttpRequestMessage createRequest(string shopNameInUrl, HttpMethod method, string action) + { + var url = $"https://{shopNameInUrl}.myshopify.com/admin/api/2020-07/" + action; + + var req = new HttpRequestMessage(method, url); + + return 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 TransactionsList(string orderId) { var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); @@ -58,21 +75,14 @@ namespace BTCPayServer.Services.Shopify return JObject.Parse(strResp); } - private HttpRequestMessage createRequest(string shopNameInUrl, HttpMethod method, string action) + public async Task OrdersCount() { - var url = $"https://{shopNameInUrl}.myshopify.com/admin/api/2020-07/" + action; + var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/count.json"); + var strResp = await sendRequest(req); - var req = new HttpRequestMessage(method, url); + dynamic parsed = JObject.Parse(strResp); - return req; - } - - private async Task sendRequest(HttpRequestMessage req) - { - using var resp = await _httpClient.SendAsync(req); - - var strResp = await resp.Content.ReadAsStringAsync(); - return strResp; + return parsed.count; } } diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml new file mode 100644 index 000000000..071a9cdf8 --- /dev/null +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -0,0 +1,87 @@ +@using static BTCPayServer.Data.StoreBlob +@model IntegrationsViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePageAndTitle(StoreNavPages.Integrations, "Integrations"); +} + + + +@if (!ViewContext.ModelState.IsValid) +{ +
+
+
+
+
+} + +
+
+
+

Shopify

+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + @if (Model.Shopify?.CredentialsValid == false) + { + + } + else + { + + } + + @if (Model.Shopify?.CredentialsValid == true) + { + var shopify = Model.Shopify; +

+

Shopify Operations

+ + if (!shopify.IntegratedAt.HasValue) + { + + } + else + { +

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

+

+ } + } + +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} From 0af4be3d9904deb83e8c047ff7525f45f7f86907 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:14:36 -0500 Subject: [PATCH 03/63] Importing hosted service that will register transactions on Shopify --- .../Shopify/OrderTransactionRegisterLogic.cs | 57 ++++++++++++ .../ShopifyOrderMarkerHostedService.cs | 89 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs create mode 100644 BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs new file mode 100644 index 000000000..eab6bca34 --- /dev/null +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Shopify +{ + public class OrderTransactionRegisterLogic + { + private readonly ShopifyApiClient _client; + + public OrderTransactionRegisterLogic(ShopifyApiClient client) + { + _client = client; + } + + public async Task Process(string orderId, string currency = null, string amountCaptured = null) + { + dynamic resp = await _client.TransactionsList(orderId); + + JArray transactions = resp.transactions; + if (transactions != null && transactions.Count >= 1) + { + dynamic transaction = transactions[0]; + + if (currency != null && currency.Equals(transaction.currency, StringComparison.OrdinalIgnoreCase)) + { + // because of parent_id present, currency will always be the one from parent transaction + // malicious attacker could potentially exploit this by creating invoice + // in different currency and paying that one, registering order on Shopify as paid + // so if currency is supplied and is different from parent transaction currency we just won't register + return null; + } + + var createTransaction = new TransactionCreate + { + transaction = new TransactionCreate.DataHolder + { + parent_id = transaction.id, + currency = transaction.currency, + amount = amountCaptured ?? transaction.amount, + kind = "capture", + gateway = "BTCPayServer", + source = "external" + } + }; + + dynamic createResp = await _client.TransactionCreate(orderId, createTransaction); + return createResp; + } + + return null; + } + } +} diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs new file mode 100644 index 000000000..c6d429304 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Logging; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBXplorer; + +namespace BTCPayServer.Services.Shopify +{ + public class ShopifyOrderMarkerHostedService : IHostedService + { + private readonly EventAggregator _eventAggregator; + private readonly StoreRepository _storeRepository; + private readonly IHttpClientFactory _httpClientFactory; + + public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator, StoreRepository storeRepository, IHttpClientFactory httpClientFactory) + { + _eventAggregator = eventAggregator; + _storeRepository = storeRepository; + _httpClientFactory = httpClientFactory; + } + + private CancellationTokenSource _Cts; + private readonly CompositeDisposable leases = new CompositeDisposable(); + + public Task StartAsync(CancellationToken cancellationToken) + { + _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + leases.Add(_eventAggregator.Subscribe(async b => + { + var invoice = b.Invoice; + var shopifyOrderId = invoice.Metadata?.OrderId; + if (invoice.Status == Client.Models.InvoiceStatus.Paid && shopifyOrderId != null) + { + var storeData = await _storeRepository.FindStore(invoice.StoreId); + var storeBlob = storeData.GetStoreBlob(); + + if (storeBlob.Shopify?.IntegratedAt.HasValue == true) + { + var client = createShopifyApiClient(storeBlob.Shopify); + + try + { + var logic = new OrderTransactionRegisterLogic(client); + await logic.Process(shopifyOrderId, invoice.Currency, invoice.Price.ToString(CultureInfo.InvariantCulture)); + Logs.PayServer.LogInformation("Registered order transaction on Shopify. " + + $"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}"); + } + } + } + })); + return Task.CompletedTask; + } + + 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) + { + _Cts?.Cancel(); + leases.Dispose(); + + return Task.CompletedTask; + } + } +} From c3d8c8d4b0d7077f66c660228e686e038ba1cc95 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:15:20 -0500 Subject: [PATCH 04/63] Checking that order exists on Shopify before registering transaction --- BTCPayServer/Services/Shopify/ShopifyApiClient.cs | 8 ++++++++ .../Services/Shopify/ShopifyOrderMarkerHostedService.cs | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index 9cc5f66b8..df04a948f 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -84,6 +84,14 @@ namespace BTCPayServer.Services.Shopify 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); + + return strResp?.Contains(orderId, StringComparison.OrdinalIgnoreCase) == true; + } } public class ShopifyApiClientCredentials diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index c6d429304..8fb4b3fdf 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -48,6 +48,11 @@ namespace BTCPayServer.Services.Shopify if (storeBlob.Shopify?.IntegratedAt.HasValue == true) { var client = createShopifyApiClient(storeBlob.Shopify); + if (!await client.OrderExists(shopifyOrderId)) + { + // don't register transactions for orders that don't exist on shopify + return; + } try { From 7a04be0f30a6efeed57a53daef865acb21b52464 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:19:14 -0500 Subject: [PATCH 05/63] Registering ShopifyOrderMarkerHostedService --- BTCPayServer/Hosting/BTCPayServerServices.cs | 4 +++- .../Services/Shopify/OrderTransactionRegisterLogic.cs | 3 --- .../Services/Shopify/ShopifyOrderMarkerHostedService.cs | 4 ---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index ef5e32576..fa55e1d19 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -25,6 +25,7 @@ using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Shopify; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using BTCPayServer.U2F; @@ -245,7 +246,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); - + + services.AddSingleton(); #if DEBUG services.AddSingleton(); #endif diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index eab6bca34..d797043ca 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 8fb4b3fdf..79df618ea 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -1,14 +1,10 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Logging; -using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; From d530059e7e464e81aef57c83fb22a74e59a96fd4 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:32:02 -0500 Subject: [PATCH 06/63] Tweaking Shopify integrations page for better display --- BTCPayServer/Views/Stores/Integrations.cshtml | 37 +++++++++---------- BTCPayServer/Views/Stores/StoreNavPages.cs | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index 071a9cdf8..3f33c658b 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -3,6 +3,10 @@ @{ Layout = "../Shared/_NavLayout.cshtml"; ViewData.SetActivePageAndTitle(StoreNavPages.Integrations, "Integrations"); + + + var shopify = Model.Shopify; + var shopifyCredsSet = shopify?.CredentialsValid == true; } @@ -24,58 +28,53 @@
- +
- +
- +
- +
- @if (Model.Shopify?.CredentialsValid == false) + @if (!shopifyCredsSet) { } - else + 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 (Model.Shopify?.CredentialsValid == true) + @if (shopifyCredsSet) { - var shopify = Model.Shopify; -

-

Shopify Operations

+
Shopify Operations
if (!shopify.IntegratedAt.HasValue) { } - else - { -

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

-

- } + } diff --git a/BTCPayServer/Views/Stores/StoreNavPages.cs b/BTCPayServer/Views/Stores/StoreNavPages.cs index ab8060240..13f1873f8 100644 --- a/BTCPayServer/Views/Stores/StoreNavPages.cs +++ b/BTCPayServer/Views/Stores/StoreNavPages.cs @@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Stores { public enum StoreNavPages { - ActivePage, Index, Rates, Checkout, Tokens, Users, PayButton + ActivePage, Index, Rates, Checkout, Tokens, Users, PayButton, Integrations } } From 05b45f8cf5a2d9edcac2b1fb3c4ed4166f5487da Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 17:35:18 -0500 Subject: [PATCH 07/63] Shopify credentials clearing that stops order marking --- BTCPayServer/Controllers/StoresController.cs | 12 ++++++++++++ BTCPayServer/Views/Stores/Integrations.cshtml | 5 +---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 593bd324f..132c036e8 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -1040,6 +1040,18 @@ namespace BTCPayServer.Controllers } 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 { diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index 3f33c658b..d038aafe7 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -63,18 +63,15 @@ Orders on @shopify.ShopName.myshopify.com will be marked as paid on successful invoice payment. Started: @shopify.IntegratedAt.Value.ToBrowserDate()

-

} @if (shopifyCredsSet) { -
Shopify Operations
- if (!shopify.IntegratedAt.HasValue) { } - + } From e4f3c1ae94f7371907d703df471849e4bb983f50 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 23:25:59 -0500 Subject: [PATCH 08/63] Tweaking UI for Shopify integration --- BTCPayServer/Views/Stores/Integrations.cshtml | 13 +++++++------ BTCPayServer/Views/Stores/_Nav.cshtml | 15 ++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index d038aafe7..722810d30 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -23,32 +23,31 @@
-

Shopify

+

+ Shopify + +

-
-
-
-
@@ -71,7 +70,9 @@ { } - + }
diff --git a/BTCPayServer/Views/Stores/_Nav.cshtml b/BTCPayServer/Views/Stores/_Nav.cshtml index 2a3fd3755..47d4c3090 100644 --- a/BTCPayServer/Views/Stores/_Nav.cshtml +++ b/BTCPayServer/Views/Stores/_Nav.cshtml @@ -1,14 +1,15 @@ From 98c530cadaec15bb0590c3a7c6dd400bbc9446fb Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 23:31:20 -0500 Subject: [PATCH 09/63] Using bundled javascript for Shopify --- BTCPayServer/bundleconfig.json | 10 +++++++++- .../wwwroot/shopify/btcpay-shopify-checkout.js | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/bundleconfig.json b/BTCPayServer/bundleconfig.json index 2e9ffeabb..464306bc3 100644 --- a/BTCPayServer/bundleconfig.json +++ b/BTCPayServer/bundleconfig.json @@ -206,5 +206,13 @@ "minify": { "enabled": false } - } + }, + { + "outputFileName": "wwwroot/bundles/shopify-bundle.min.js", + "inputFiles": [ + "wwwroot/modal/btcpay.js", + "wwwroot/shopify/btcpay-browser-client.js", + "wwwroot/shopify/btcpay-shopify-checkout.js" + ] + } ] diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js index 54203f898..413dcff67 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js @@ -9,9 +9,7 @@ const BTCPAYSERVER_URL = "FULL_BTCPAYSERVER_URL_WITH_HTTPS"; const STORE_ID = "YOUR_BTCPAY_STORE_ID"; - - - + */ From c39138e3bab1535ac1ecbe408b03ebc7b3566c3b Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 23:44:57 -0500 Subject: [PATCH 10/63] Not displaying Shopify Api Password --- BTCPayServer/Views/Stores/Integrations.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index 722810d30..b034fdcff 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -42,7 +42,7 @@
- +
From 0818abe233d7a769cf82323363041ec5c1f58a53 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 13 Sep 2020 23:54:15 -0500 Subject: [PATCH 11/63] Prefixing Shopify invoices, passing real orderId --- .../Services/Shopify/ShopifyOrderMarkerHostedService.cs | 7 ++++++- BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js | 7 +++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 79df618ea..853877c70 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -28,6 +28,8 @@ namespace BTCPayServer.Services.Shopify private CancellationTokenSource _Cts; private readonly CompositeDisposable leases = new CompositeDisposable(); + public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-"; + public Task StartAsync(CancellationToken cancellationToken) { _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -41,8 +43,11 @@ namespace BTCPayServer.Services.Shopify var storeData = await _storeRepository.FindStore(invoice.StoreId); var storeBlob = storeData.GetStoreBlob(); - if (storeBlob.Shopify?.IntegratedAt.HasValue == true) + if (storeBlob.Shopify?.IntegratedAt.HasValue == true && + shopifyOrderId.StartsWith(SHOPIFY_ORDER_ID_PREFIX, StringComparison.OrdinalIgnoreCase)) { + shopifyOrderId = shopifyOrderId[SHOPIFY_ORDER_ID_PREFIX.Length..]; + var client = createShopifyApiClient(storeBlob.Shopify); if (!await client.OrderExists(shopifyOrderId)) { diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js index 413dcff67..200804137 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify-checkout.js @@ -17,6 +17,7 @@ // extracted from shopify initialized page const shopify_price = Shopify.checkout.payment_due; const shopify_currency = Shopify.checkout.currency; + const shopify_order_id = "shopify-" + Shopify.checkout.order_id; "use strict"; const pageElements = document.querySelector.bind(document), @@ -79,9 +80,7 @@ pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "none"), pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "none"); - const orderId = pageItems.orderNumber.innerText.replace("Order #", ""); - - const url = BTCPAYSERVER_URL + "/invoices" + "?storeId=" + STORE_ID + "&orderId=" + orderId + "&status=complete"; + const url = BTCPAYSERVER_URL + "/invoices" + "?storeId=" + STORE_ID + "&orderId=" + shopify_order_id + "&status=complete"; // Check if already paid. fetch(url, { @@ -112,7 +111,7 @@ { price: shopify_price, currency: shopify_currency, - orderId: orderId + orderId: shopify_order_id } ) .then(function (invoice) { From 858d56d20c0343fea8859993d2b21d539dd4c1a8 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 14 Sep 2020 00:04:56 -0500 Subject: [PATCH 12/63] Bugfixing problem with currency comparison --- BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index d797043ca..1ae80fc16 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Shopify { dynamic transaction = transactions[0]; - if (currency != null && currency.Equals(transaction.currency, StringComparison.OrdinalIgnoreCase)) + if (currency != null && currency.ToUpperInvariant().Trim() != transaction.currency.ToString().ToUpperInvariant().Trim()) { // because of parent_id present, currency will always be the one from parent transaction // malicious attacker could potentially exploit this by creating invoice From a93e3be197caa6dd1de468bf8b807a9d5fb0b2f3 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 14 Sep 2020 00:49:28 -0500 Subject: [PATCH 13/63] Initializing modal apiPrefix url, needed for bundling --- BTCPayServer/wwwroot/shopify/btcpay-browser-client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js index d73aa9804..8ff66ef02 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js @@ -16,6 +16,7 @@ var BtcPayServerModal = (function () { else { waitForPayment.lock = true; } + window.btcpay.setApiUrlPrefix(btcpayServerUrl); window.btcpay.onModalWillEnter(function () { var interval = setInterval(function () { getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId) From b993da9751f994acff6d1875414738a63b7d3944 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 14 Sep 2020 01:06:23 -0500 Subject: [PATCH 14/63] Ensuring that Shopify.Password values is present --- BTCPayServer/Views/Stores/Integrations.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index b034fdcff..330df5157 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -42,7 +42,7 @@
- +
From ce15eed34380906fe4ac9205583d70a040ec8640 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 14 Sep 2020 23:37:15 -0500 Subject: [PATCH 15/63] Fixing javascript variable typo --- BTCPayServer/wwwroot/shopify/btcpay-browser-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js index 8ff66ef02..f4b1ee530 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js @@ -16,7 +16,7 @@ var BtcPayServerModal = (function () { else { waitForPayment.lock = true; } - window.btcpay.setApiUrlPrefix(btcpayServerUrl); + window.btcpay.setApiUrlPrefix(btcPayServerUrl); window.btcpay.onModalWillEnter(function () { var interval = setInterval(function () { getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId) From fa08c52f7498fc4bb02d39accff243215cc32276 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 14 Sep 2020 23:49:05 -0500 Subject: [PATCH 16/63] Bugfixing detection of invoice paid in shopify modal --- BTCPayServer/wwwroot/shopify/btcpay-browser-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js index f4b1ee530..fca171372 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js @@ -21,7 +21,7 @@ var BtcPayServerModal = (function () { var interval = setInterval(function () { getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId) .then(function (invoice) { - if (invoice.status == "complete") { + if (invoice.status === "complete" || invoice.status === "paid") { clearInterval(interval); resolve(invoice); } From 9b0fb9ecdcc1e3bc24df5117abea35d45a27d7f2 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 16 Sep 2020 23:42:46 -0500 Subject: [PATCH 17/63] Adding links to other integrations per @pavlenex suggestion --- BTCPayServer/Views/Stores/Integrations.cshtml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/BTCPayServer/Views/Stores/Integrations.cshtml b/BTCPayServer/Views/Stores/Integrations.cshtml index 330df5157..11fd2400b 100644 --- a/BTCPayServer/Views/Stores/Integrations.cshtml +++ b/BTCPayServer/Views/Stores/Integrations.cshtml @@ -76,6 +76,19 @@ } + +

+ Other Integrations +

+

+ Take a look at documentation for the list of other integrations we support and the directions on how to enable them: +

+

From 0e53ead14e69755dc8a8bf2a714b242bef5a398b Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 16 Sep 2020 23:44:21 -0500 Subject: [PATCH 18/63] Adding another state for detection of invoice paid --- BTCPayServer/wwwroot/shopify/btcpay-browser-client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js index fca171372..77d2978ff 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-browser-client.js @@ -21,7 +21,8 @@ var BtcPayServerModal = (function () { var interval = setInterval(function () { getBtcPayInvoice(btcPayServerUrl, invoiceId, storeId) .then(function (invoice) { - if (invoice.status === "complete" || invoice.status === "paid") { + // in most cases this will be triggered by paid, but we put other statuses just in case + if (invoice.status === "paid" || invoice.status === "complete" || invoice.status === "confirmed") { clearInterval(interval); resolve(invoice); } From f177a39b96bd857c3c3167ed93dfdb9a13c534ef Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 17 Sep 2020 00:12:03 -0500 Subject: [PATCH 19/63] Removing dynamic variables from Shopify api interactions --- .../DataHolders/TransactionDataHolder.cs | 33 +++++++++++++++++++ .../Shopify/ApiModels/OrdersCountResp.cs | 13 ++++++++ .../ApiModels/TransactionsCreateReq.cs | 24 ++++++++++++++ .../ApiModels/TransactionsCreateResp.cs | 14 ++++++++ .../Shopify/ApiModels/TransactionsListResp.cs | 14 ++++++++ .../Shopify/OrderTransactionRegisterLogic.cs | 15 +++++---- .../Services/Shopify/ShopifyApiClient.cs | 28 ++++------------ .../ShopifyOrderMarkerHostedService.cs | 5 ++- 8 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 BTCPayServer/Services/Shopify/ApiModels/DataHolders/TransactionDataHolder.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateResp.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs diff --git a/BTCPayServer/Services/Shopify/ApiModels/DataHolders/TransactionDataHolder.cs b/BTCPayServer/Services/Shopify/ApiModels/DataHolders/TransactionDataHolder.cs new file mode 100644 index 000000000..136b4949d --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/DataHolders/TransactionDataHolder.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Shopify.ApiModels.DataHolders +{ + public class TransactionDataHolder + { + public long id { get; set; } + public long? order_id { get; set; } + public string kind { get; set; } + public string gateway { get; set; } + public string status { get; set; } + public string message { get; set; } + public DateTimeOffset created_at { get; set; } + public bool test { get; set; } + public string authorization { get; set; } + public string location_id { get; set; } + public string user_id { get; set; } + public long? parent_id { get; set; } + public DateTimeOffset processed_at { get; set; } + public string device_id { get; set; } + public object receipt { get; set; } + public string error_code { get; set; } + public string source_name { get; set; } + public string currency_exchange_adjustment { get; set; } + public string amount { get; set; } + public string currency { get; set; } + public string admin_graphql_api_id { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs b/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs new file mode 100644 index 000000000..b34ad4e7c --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/OrdersCountResp.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class OrdersCountResp + { + public long count { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs new file mode 100644 index 000000000..3720b81ea --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateReq.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class TransactionsCreateReq + { + public DataHolder transaction { get; set; } + + public class DataHolder + { + public string currency { get; set; } + public string amount { get; set; } + public string kind { get; set; } + public long? parent_id { get; set; } + public string gateway { get; set; } + public string source { get; set; } + public string authorization { get; set; } + } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateResp.cs b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateResp.cs new file mode 100644 index 000000000..ae38f3f60 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/TransactionsCreateResp.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Services.Shopify.ApiModels.DataHolders; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class TransactionsCreateResp + { + public TransactionDataHolder transaction { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs b/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs new file mode 100644 index 000000000..4b853a228 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Services.Shopify.ApiModels.DataHolders; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class TransactionsListResp + { + public List transactions { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 1ae80fc16..5134f7638 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -1,5 +1,8 @@ using System; +using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Services.Shopify.ApiModels; +using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Shopify @@ -15,12 +18,12 @@ namespace BTCPayServer.Services.Shopify public async Task Process(string orderId, string currency = null, string amountCaptured = null) { - dynamic resp = await _client.TransactionsList(orderId); + var resp = await _client.TransactionsList(orderId); - JArray transactions = resp.transactions; - if (transactions != null && transactions.Count >= 1) + var txns = resp.transactions; + if (txns != null && txns.Count >= 1) { - dynamic transaction = transactions[0]; + var transaction = txns[0]; if (currency != null && currency.ToUpperInvariant().Trim() != transaction.currency.ToString().ToUpperInvariant().Trim()) { @@ -31,9 +34,9 @@ namespace BTCPayServer.Services.Shopify return null; } - var createTransaction = new TransactionCreate + var createTransaction = new TransactionsCreateReq { - transaction = new TransactionCreate.DataHolder + transaction = new TransactionsCreateReq.DataHolder { parent_id = transaction.id, currency = transaction.currency, diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index df04a948f..8142db419 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -4,6 +4,7 @@ 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; @@ -53,18 +54,18 @@ namespace BTCPayServer.Services.Shopify return strResp; } - public async Task TransactionsList(string orderId) + public async Task TransactionsList(string orderId) { var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); var strResp = await sendRequest(req); - dynamic parsed = JObject.Parse(strResp); + var parsed = JsonConvert.DeserializeObject(strResp); return parsed; } - public async Task TransactionCreate(string orderId, TransactionCreate txnCreate) + public async Task TransactionCreate(string orderId, TransactionsCreateReq txnCreate) { var postJson = JsonConvert.SerializeObject(txnCreate); @@ -72,15 +73,15 @@ namespace BTCPayServer.Services.Shopify req.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); var strResp = await sendRequest(req); - return JObject.Parse(strResp); + return JsonConvert.DeserializeObject(strResp); } - public async Task OrdersCount() + public async Task OrdersCount() { var req = createRequest(_creds.ShopName, HttpMethod.Get, $"orders/count.json"); var strResp = await sendRequest(req); - dynamic parsed = JObject.Parse(strResp); + var parsed = JsonConvert.DeserializeObject(strResp); return parsed.count; } @@ -101,19 +102,4 @@ namespace BTCPayServer.Services.Shopify public string ApiPassword { get; set; } public string SharedSecret { get; set; } } - - public class TransactionCreate - { - public DataHolder transaction { get; set; } - - public class DataHolder - { - public string currency { get; set; } - public string amount { get; set; } - public string kind { get; set; } - public string parent_id { get; set; } - public string gateway { get; set; } - public string source { get; set; } - } - } } diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 853877c70..084bf9b44 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -38,7 +38,10 @@ namespace BTCPayServer.Services.Shopify { var invoice = b.Invoice; var shopifyOrderId = invoice.Metadata?.OrderId; - if (invoice.Status == Client.Models.InvoiceStatus.Paid && shopifyOrderId != null) + // TODO: Don't code on live webcast, take time offline with Kukks to verify all flows + // Lightning it can just be paid + if ((invoice.Status == Client.Models.InvoiceStatus.Complete || invoice.Status == Client.Models.InvoiceStatus.Confirmed) + && shopifyOrderId != null) { var storeData = await _storeRepository.FindStore(invoice.StoreId); var storeBlob = storeData.GetStoreBlob(); From d9872bc94d3fdddf0ca55a3dc9a3051c01e758bb Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 17 Sep 2020 00:18:18 -0500 Subject: [PATCH 20/63] Making sure we don't register same invoice multiple times --- .../Shopify/OrderTransactionRegisterLogic.cs | 12 +++++++----- .../Shopify/ShopifyOrderMarkerHostedService.cs | 9 ++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 5134f7638..388be66e7 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -16,16 +16,17 @@ namespace BTCPayServer.Services.Shopify _client = client; } - public async Task Process(string orderId, string currency = null, string amountCaptured = null) + public async Task Process(string orderId, string invoiceId, string currency = null, string amountCaptured = null) { var resp = await _client.TransactionsList(orderId); var txns = resp.transactions; - if (txns != null && txns.Count >= 1) + // only register transactions if first, parent_id transaction is present and we haven't already registered transaction for this invoice + if (txns != null && txns.Count >= 1 && !txns.Any(a => a.authorization == invoiceId)) { var transaction = txns[0]; - if (currency != null && currency.ToUpperInvariant().Trim() != transaction.currency.ToString().ToUpperInvariant().Trim()) + if (currency != null && currency.ToUpperInvariant().Trim() != transaction.currency.ToUpperInvariant().Trim()) { // because of parent_id present, currency will always be the one from parent transaction // malicious attacker could potentially exploit this by creating invoice @@ -43,11 +44,12 @@ namespace BTCPayServer.Services.Shopify amount = amountCaptured ?? transaction.amount, kind = "capture", gateway = "BTCPayServer", - source = "external" + source = "external", + authorization = invoiceId } }; - dynamic createResp = await _client.TransactionCreate(orderId, createTransaction); + var createResp = await _client.TransactionCreate(orderId, createTransaction); return createResp; } diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 084bf9b44..8091bf3c7 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -61,9 +61,12 @@ namespace BTCPayServer.Services.Shopify try { var logic = new OrderTransactionRegisterLogic(client); - await logic.Process(shopifyOrderId, invoice.Currency, invoice.Price.ToString(CultureInfo.InvariantCulture)); - Logs.PayServer.LogInformation("Registered order transaction on Shopify. " + - $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); + 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}"); + } } catch (Exception ex) { From 5b958d7ff66e92fab338e7fe90f027d16638d0fd Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 17 Sep 2020 00:24:26 -0500 Subject: [PATCH 21/63] Ensuring that Shopify transaction registrations are not done in parallel --- .../Shopify/ShopifyOrderMarkerHostedService.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index 8091bf3c7..eb60a7d4e 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -30,6 +30,9 @@ namespace BTCPayServer.Services.Shopify public const string SHOPIFY_ORDER_ID_PREFIX = "shopify-"; + + private static readonly SemaphoreSlim _shopifyEventsSemaphore = new SemaphoreSlim(1, 1); + public Task StartAsync(CancellationToken cancellationToken) { _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -40,7 +43,7 @@ namespace BTCPayServer.Services.Shopify var shopifyOrderId = invoice.Metadata?.OrderId; // TODO: Don't code on live webcast, take time offline with Kukks to verify all flows // Lightning it can just be paid - if ((invoice.Status == Client.Models.InvoiceStatus.Complete || invoice.Status == Client.Models.InvoiceStatus.Confirmed) + if ((invoice.Status == Client.Models.InvoiceStatus.Complete || invoice.Status == Client.Models.InvoiceStatus.Confirmed) && shopifyOrderId != null) { var storeData = await _storeRepository.FindStore(invoice.StoreId); @@ -49,6 +52,8 @@ 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); @@ -60,6 +65,8 @@ namespace BTCPayServer.Services.Shopify try { + await _shopifyEventsSemaphore.WaitAsync(); + var logic = new OrderTransactionRegisterLogic(client); var resp = await logic.Process(shopifyOrderId, invoice.Id, invoice.Currency, invoice.Price.ToString(CultureInfo.InvariantCulture)); if (resp != null) @@ -73,6 +80,10 @@ namespace BTCPayServer.Services.Shopify Logs.PayServer.LogError(ex, $"Shopify error while trying to register order transaction. " + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); } + finally + { + _shopifyEventsSemaphore.Release(); + } } } })); From 1c510d45f0e84fff65be6f062328c8bdf349aa84 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 17 Sep 2020 00:29:33 -0500 Subject: [PATCH 22/63] Commenting code to elaborate on decisions made --- .../Services/Shopify/ShopifyOrderMarkerHostedService.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index eb60a7d4e..36023ced3 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -41,14 +41,15 @@ namespace BTCPayServer.Services.Shopify { var invoice = b.Invoice; var shopifyOrderId = invoice.Metadata?.OrderId; - // TODO: Don't code on live webcast, take time offline with Kukks to verify all flows - // Lightning it can just be paid - if ((invoice.Status == Client.Models.InvoiceStatus.Complete || invoice.Status == Client.Models.InvoiceStatus.Confirmed) + // 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) && shopifyOrderId != null) { var storeData = await _storeRepository.FindStore(invoice.StoreId); var storeBlob = storeData.GetStoreBlob(); + // ensure that store in question has shopify integration turned on + // and that invoice's orderId has shopify specific prefix if (storeBlob.Shopify?.IntegratedAt.HasValue == true && shopifyOrderId.StartsWith(SHOPIFY_ORDER_ID_PREFIX, StringComparison.OrdinalIgnoreCase)) { @@ -63,6 +64,8 @@ namespace BTCPayServer.Services.Shopify return; } + // if we got this far, we likely need to register this invoice's payment on Shopify + // OrderTransactionRegisterLogic has check if transaction is already registered which is why we're passing invoice.Id try { await _shopifyEventsSemaphore.WaitAsync(); From 0cf9b20328cfce6e40b26388bb5eb15979a6d97f Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 18 Sep 2020 17:20:31 +0200 Subject: [PATCH 23/63] Kukks Shopify Enhancement Suite Commit --- .../StoresController.Integrations.cs | 176 ++++++++++++++++++ BTCPayServer/Controllers/StoresController.cs | 102 +--------- BTCPayServer/Data/StoreBlob.cs | 23 +-- BTCPayServer/Hosting/BTCPayServerServices.cs | 2 +- .../StoreViewModels/IntegrationsViewModel.cs | 3 +- .../Shopify/ApiModels/CreateScriptResponse.cs | 31 +++ .../ApiModels/CreateWebhookResponse.cs | 35 ++++ .../Shopify/ApiModels/OrdersCountResp.cs | 11 +- .../ApiModels/TransactionsCreateReq.cs | 6 - .../Shopify/Models/ShopifySettings.cs | 25 +++ .../Services/Shopify/ShopifyApiClient.cs | 83 ++++++--- .../Services/Shopify/ShopifyExtensions.cs | 24 +++ .../ShopifyOrderMarkerHostedService.cs | 73 +++----- BTCPayServer/Views/Stores/Integrations.cshtml | 60 +----- .../Views/Stores/Integrations/Shopify.cshtml | 102 ++++++++++ .../wwwroot/shopify/btcpay-browser-client.js | 19 +- 16 files changed, 509 insertions(+), 266 deletions(-) create mode 100644 BTCPayServer/Controllers/StoresController.Integrations.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/CreateScriptResponse.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/CreateWebhookResponse.cs create mode 100644 BTCPayServer/Services/Shopify/Models/ShopifySettings.cs create mode 100644 BTCPayServer/Services/Shopify/ShopifyExtensions.cs create mode 100644 BTCPayServer/Views/Stores/Integrations/Shopify.cshtml 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); From 4516bbdadd723337b971117086f63b56ee63d536 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sat, 19 Sep 2020 12:13:55 +0200 Subject: [PATCH 24/63] Fix and Cache Shopify JS --- .../StoresController.Integrations.cs | 35 +++++++++++-------- BTCPayServer/Controllers/StoresController.cs | 10 +----- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index f13b0da0a..2e912cf2f 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -10,37 +10,42 @@ 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[] + private static string _cachedBasejavascript; + + private async Task GetJavascript() + { + if (!string.IsNullOrEmpty(_cachedBasejavascript)) { - "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}; + return _cachedBasejavascript; } - var jsFile = $"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\";"; + string[] fileList = _BtcpayServerOptions.BundleJsCss + ? new[] { "bundles/shopify-bundle.min.js"} + : new[] {"modal/btcpay.js", "shopify/btcpay-browser-client.js", "shopify/btcpay-shopify-checkout.js"}; + + 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(); + _cachedBasejavascript += Environment.NewLine + await reader.ReadToEndAsync(); } + return _cachedBasejavascript; + } + + [AllowAnonymous] + [HttpGet("{storeId}/integrations/shopify/shopify.js")] + public async Task ShopifyJavascript(string storeId) + { + var jsFile = $"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\"; { await GetJavascript()}"; return Content(jsFile, "text/javascript"); } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 237529b3a..1d653ba5a 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -58,15 +58,13 @@ namespace BTCPayServer.Controllers ExplorerClientProvider explorerProvider, IFeeProviderFactory feeRateProvider, LanguageService langService, - IWebHostEnvironment env, IHttpClientFactory httpClientFactory, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, SettingsRepository settingsRepository, IAuthorizationService authorizationService, EventAggregator eventAggregator, CssThemeManager cssThemeManager, AppService appService, - IWebHostEnvironment webHostEnvironment, - IBundleProvider bundleProvider) + IWebHostEnvironment webHostEnvironment) { _RateFactory = rateFactory; _Repo = repo; @@ -75,15 +73,12 @@ namespace BTCPayServer.Controllers _LangService = langService; _TokenController = tokenController; _WalletProvider = walletProvider; - _Env = env; - _httpClientFactory = httpClientFactory; _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _settingsRepository = settingsRepository; _authorizationService = authorizationService; _CssThemeManager = cssThemeManager; _appService = appService; _webHostEnvironment = webHostEnvironment; - _bundleProvider = bundleProvider; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; @@ -105,15 +100,12 @@ namespace BTCPayServer.Controllers readonly TokenRepository _TokenRepository; readonly UserManager _UserManager; private readonly LanguageService _LangService; - readonly IWebHostEnvironment _Env; - private readonly IHttpClientFactory _httpClientFactory; private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly SettingsRepository _settingsRepository; 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] From 19cc93daaebb8c8134c8e6459d991b6a989cfa5d Mon Sep 17 00:00:00 2001 From: Kukks Date: Sat, 19 Sep 2020 16:53:45 +0200 Subject: [PATCH 25/63] Reduce steps to 1 when integrating Shopify and start adding more API calls for Shopify to reduce JS logic --- .../StoresController.Integrations.cs | 47 +++++------- BTCPayServer/Controllers/StoresController.cs | 3 - .../Shopify/ApiModels/TransactionsListResp.cs | 4 - .../Services/Shopify/ShopifyApiClient.cs | 76 +++++++++++++------ .../Views/Stores/Integrations/Shopify.cshtml | 6 +- 5 files changed, 73 insertions(+), 63 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 2e912cf2f..e5a84f6f9 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -1,28 +1,32 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Shopify; using BTCPayServer.Services.Shopify.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Nethereum.Util; namespace BTCPayServer.Controllers { public partial class StoresController { - private static string _cachedBasejavascript; + private static string _cachedShopifyJavascript; private async Task GetJavascript() { - if (!string.IsNullOrEmpty(_cachedBasejavascript)) + if (!string.IsNullOrEmpty(_cachedShopifyJavascript)) { - return _cachedBasejavascript; + return _cachedShopifyJavascript; } string[] fileList = _BtcpayServerOptions.BundleJsCss @@ -35,10 +39,10 @@ namespace BTCPayServer.Controllers await using var stream = _webHostEnvironment.WebRootFileProvider .GetFileInfo(file).CreateReadStream(); using var reader = new StreamReader(stream); - _cachedBasejavascript += Environment.NewLine + await reader.ReadToEndAsync(); + _cachedShopifyJavascript += Environment.NewLine + await reader.ReadToEndAsync(); } - return _cachedBasejavascript; + return _cachedShopifyJavascript; } [AllowAnonymous] @@ -70,7 +74,7 @@ namespace BTCPayServer.Controllers { try { -//https://{apikey}:{password}@{hostname}/admin/api/{version}/{resource}.json + //https://{apikey}:{password}@{hostname}/admin/api/{version}/{resource}.json var parsedUrl = new Uri(exampleUrl); var userInfo = parsedUrl.UserInfo.Split(":"); vm.Shopify = new ShopifySettings() @@ -108,12 +112,20 @@ namespace BTCPayServer.Controllers catch { TempData[WellKnownTempData.ErrorMessage] = - "Shopify rejected provided credentials, please correct values and again"; + "Shopify rejected provided credentials, please correct values and try again"; return View("Integrations", vm); } shopify.CredentialsValid = true; + var scopesGranted = await apiClient.CheckScopes(); + if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_script_tags")) + { + TempData[WellKnownTempData.ErrorMessage] = + "Please grant the private app permissions for read_orders, write_script_tags"; + return View("Integrations", vm); + } + var blob = CurrentStore.GetStoreBlob(); blob.Shopify = shopify; if (CurrentStore.SetStoreBlob(blob)) @@ -121,26 +133,7 @@ namespace BTCPayServer.Controllers 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"; + TempData[WellKnownTempData.SuccessMessage] = "Shopify integration successfully updated"; break; } case "ShopifyClearCredentials": diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 1d653ba5a..b828e9514 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -56,7 +56,6 @@ namespace BTCPayServer.Controllers BTCPayNetworkProvider networkProvider, RateFetcher rateFactory, ExplorerClientProvider explorerProvider, - IFeeProviderFactory feeRateProvider, LanguageService langService, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, SettingsRepository settingsRepository, @@ -82,7 +81,6 @@ namespace BTCPayServer.Controllers _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; - _FeeRateProvider = feeRateProvider; _ServiceProvider = serviceProvider; _BtcpayServerOptions = btcpayServerOptions; _BTCPayEnv = btcpayEnv; @@ -93,7 +91,6 @@ namespace BTCPayServer.Controllers readonly IServiceProvider _ServiceProvider; readonly BTCPayNetworkProvider _NetworkProvider; private readonly ExplorerClientProvider _ExplorerProvider; - private readonly IFeeProviderFactory _FeeRateProvider; readonly BTCPayWalletProvider _WalletProvider; readonly AccessTokenController _TokenController; readonly StoreRepository _Repo; diff --git a/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs b/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs index 4b853a228..595d84f5f 100644 --- a/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs +++ b/BTCPayServer/Services/Shopify/ApiModels/TransactionsListResp.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using BTCPayServer.Services.Shopify.ApiModels.DataHolders; namespace BTCPayServer.Services.Shopify.ApiModels diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index cac85a635..5da3df3ac 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -1,10 +1,13 @@ 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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Shopify { @@ -23,6 +26,7 @@ namespace BTCPayServer.Services.Shopify { _httpClient = new HttpClient(); } + _credentials = credentials; var bearer = $"{credentials.ApiKey}:{credentials.ApiPassword}"; @@ -31,9 +35,11 @@ namespace BTCPayServer.Services.Shopify _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer); } - private HttpRequestMessage CreateRequest(string shopName, HttpMethod method, string action) + private HttpRequestMessage CreateRequest(string shopName, HttpMethod method, string action, + string relativeUrl = null) { - var url = $"https://{(shopName.Contains(".", StringComparison.InvariantCulture)? shopName: $"{shopName}.myshopify.com")}/admin/api/2020-07/" + action; + var url = + $"https://{(shopName.Contains(".", StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ("admin/api/2020-07/" + action)}"; var req = new HttpRequestMessage(method, url); return req; } @@ -45,16 +51,12 @@ namespace BTCPayServer.Services.Shopify 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"); + req.Content = new StringContent(JsonConvert.SerializeObject(new {topic, address, format}), Encoding.UTF8, + "application/json"); var strResp = await SendRequest(req); return JsonConvert.DeserializeObject(strResp); @@ -63,18 +65,24 @@ namespace BTCPayServer.Services.Shopify public async Task RemoveWebhook(string id) { var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"webhooks/{id}.json"); - await SendRequest(req); + var strResp = await SendRequest(req); } - public async Task CreateScript(string scriptUrl, string evt = "onload", string scope = "order_status") + public async Task CheckScopes() + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, null, "admin/oauth/access_scopes.json"); + return JObject.Parse(await SendRequest(req))["access_scopes"].Values() + .Select(token => token["handle"].Value()).ToArray(); + } + + 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"); + 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); @@ -108,6 +116,26 @@ namespace BTCPayServer.Services.Shopify return JsonConvert.DeserializeObject(strResp); } + public async Task UpdateOrderNote(string orderId, string note) + { + var postJson = JsonConvert.SerializeObject(new {order = new {id = orderId, note}}); + + var req = CreateRequest(_credentials.ShopName, HttpMethod.Put, $"orders/{orderId}.json"); + req.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); + + await SendRequest(req); + } + + public async Task GetOrder(string orderId) + { + var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, + $"orders/{orderId}.json?fields=id,total_price,currency,transactions,financial_status"); + + var strResp = await SendRequest(req); + + return JsonConvert.DeserializeObject(strResp); + } + public async Task OrdersCount() { var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, $"orders/count.json"); @@ -126,11 +154,11 @@ namespace BTCPayServer.Services.Shopify return strResp?.Contains(orderId, StringComparison.OrdinalIgnoreCase) == true; } } - - public class ShopifyApiClientCredentials - { - public string ShopName { get; set; } - public string ApiKey { get; set; } - public string ApiPassword { get; set; } - } +} + +public class ShopifyApiClientCredentials +{ + public string ShopName { get; set; } + public string ApiKey { get; set; } + public string ApiPassword { get; set; } } diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index b8b85c213..2c56a5778 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -81,7 +81,7 @@ function promptExampleUrl(){ } else { -

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

+

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 @@ -90,10 +90,6 @@ function promptExampleUrl(){ @if (shopifyCredsSet) { - if (!shopify.IntegratedAt.HasValue) - { - - } From f77c27ca71d51705089f6219e2ec10be96aad934 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sat, 19 Sep 2020 16:54:09 +0200 Subject: [PATCH 26/63] Make BTCPay Modal refer to mattermost instead of slack --- BTCPayServer/wwwroot/modal/btcpay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/modal/btcpay.js b/BTCPayServer/wwwroot/modal/btcpay.js index 15a972b81..358ec14ef 100644 --- a/BTCPayServer/wwwroot/modal/btcpay.js +++ b/BTCPayServer/wwwroot/modal/btcpay.js @@ -40,7 +40,7 @@ iframe.style.width = '100%'; iframe.style.zIndex = '2000'; - var origin = 'http://slack.btcpayserver.org join us there, and initialize this with your origin url through setApiUrlPrefix'; + var origin = 'http://chat.btcpayserver.org join us there, and initialize this with your origin url through setApiUrlPrefix'; var scriptMatch = thisScript.match(scriptSrcRegex) if (scriptMatch) { // We can't just take the domain as btcpay can run under a sub path with RootPath From 91da129abcd637351d385c9b565d73251cc960e4 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sat, 19 Sep 2020 16:54:40 +0200 Subject: [PATCH 27/63] Include missing Shopify API models --- .../Shopify/ApiModels/ShopifyOrder.cs | 19 ++++++++ .../Shopify/ApiModels/ShopifyTransaction.cs | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 BTCPayServer/Services/Shopify/ApiModels/ShopifyOrder.cs create mode 100644 BTCPayServer/Services/Shopify/ApiModels/ShopifyTransaction.cs diff --git a/BTCPayServer/Services/Shopify/ApiModels/ShopifyOrder.cs b/BTCPayServer/Services/Shopify/ApiModels/ShopifyOrder.cs new file mode 100644 index 000000000..1b7e513fe --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/ShopifyOrder.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class ShopifyOrder + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("total_price")] + public decimal TotalPrice { get; set; } + [JsonProperty("currency")] + public string Currency { get; set; } + [JsonProperty("financial_status")] + public string FinancialStatus { get; set; } + [JsonProperty("transactions")] + public IEnumerable Transactions { get; set; } + } +} diff --git a/BTCPayServer/Services/Shopify/ApiModels/ShopifyTransaction.cs b/BTCPayServer/Services/Shopify/ApiModels/ShopifyTransaction.cs new file mode 100644 index 000000000..80ed1842c --- /dev/null +++ b/BTCPayServer/Services/Shopify/ApiModels/ShopifyTransaction.cs @@ -0,0 +1,48 @@ +using System; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Shopify.ApiModels +{ + public class ShopifyTransaction + { + [JsonProperty("amount")] + public decimal? Amount { get; set; } + + [JsonProperty("authorization")] + public string Authorization { get; set; } + + [JsonProperty("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + [JsonProperty("device_id")] + public string DeviceId { get; set; } + + [JsonProperty("gateway")] + public string Gateway { get; set; } + [JsonProperty("kind")] + public string Kind { get; set; } + [JsonProperty("order_id")] + public long? OrderId { get; set; } + + ///

+ /// A standardized error code, e.g. 'incorrect_number', independent of the payment provider. Value can be null. A full list of known values can be found at https://help.shopify.com/api/reference/transaction. + /// + [JsonProperty("error_code")] + public string ErrorCode { get; set; } + + /// + /// The status of the transaction. Valid values are: pending, failure, success or error. + /// + [JsonProperty("status")] + public string Status { get; set; } + [JsonProperty("test")] + public bool? Test { get; set; } + [JsonProperty("currency")] + public string Currency { get; set; } + /// + /// This property is undocumented by Shopify. + /// + [JsonProperty("parent_id")] + public long? ParentId { get; set; } + } +} From 3054f91fe7ad8c84cc4b816f2186ec183ce3eb94 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 10:24:56 +0200 Subject: [PATCH 28/63] Fix typo in Invoice method param --- BTCPayServer/Services/Invoices/InvoiceEntity.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index aff8665a4..cd80d7d31 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -259,11 +259,11 @@ namespace BTCPayServer.Services.Invoices [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public HashSet InternalTags { get; set; } = new HashSet(); - public string[] GetInternalTags(string suffix) + public string[] GetInternalTags(string prefix) { return InternalTags == null ? Array.Empty() : InternalTags - .Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture)) - .Select(t => t.Substring(suffix.Length)).ToArray(); + .Where(t => t.StartsWith(prefix, StringComparison.InvariantCulture)) + .Select(t => t.Substring(prefix.Length)).ToArray(); } [Obsolete("Use GetDerivationStrategies instead")] From 9aed4b0e87cac10c55eb2a2973a8197265a931bd Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 12:32:52 +0200 Subject: [PATCH 29/63] Allow receiving events from the BTCPay modal library --- BTCPayServer/wwwroot/modal/btcpay.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/modal/btcpay.js b/BTCPayServer/wwwroot/modal/btcpay.js index 358ec14ef..14a64d69f 100644 --- a/BTCPayServer/wwwroot/modal/btcpay.js +++ b/BTCPayServer/wwwroot/modal/btcpay.js @@ -56,6 +56,7 @@ var onModalWillEnterMethod = function () { }; var onModalWillLeaveMethod = function () { }; + var onModalReceiveMessageMethod = function (event) { }; function showFrame() { if (window.document.getElementsByName('btcpay').length === 0) { @@ -80,6 +81,10 @@ onModalWillLeaveMethod = customOnModalWillLeave; } + function onModalReceiveMessage(customOnModalReceiveMessage) { + onModalReceiveMessageMethod = customOnModalReceiveMessage; + } + function receiveMessage(event) { var uri; @@ -101,6 +106,7 @@ window.location = uri; } } + onModalReceiveMessageMethod(event); } function showInvoice(invoiceId, params) { @@ -134,7 +140,8 @@ showInvoice: showInvoice, onModalWillEnter: onModalWillEnter, onModalWillLeave: onModalWillLeave, - setApiUrlPrefix: setApiUrlPrefix + setApiUrlPrefix: setApiUrlPrefix, + onModalReceiveMessage: onModalReceiveMessage }; })(); From 0b2f115b73147f35e0549a796a1dfd7353977936 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 12:33:46 +0200 Subject: [PATCH 30/63] Fix issue with new Invoice API ignoring internal tags --- BTCPayServer/Controllers/InvoiceController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index c4eba1e73..d86a4f862 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -175,6 +175,8 @@ namespace BTCPayServer.Controllers excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); } entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance; + if (additionalTags != null) + entity.InternalTags.AddRange(additionalTags); return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken); } From f352399428f5aacfaa237061488771f023c7f7fd Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 12:34:09 +0200 Subject: [PATCH 31/63] Make Invoices save internal tags in the text search --- BTCPayServer/Services/Invoices/InvoiceRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 8c40e40f7..4c6ec3331 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -204,7 +204,7 @@ retry: textSearch.Add(invoice.Metadata.OrderId); textSearch.Add(ToString(invoice.Metadata, null)); textSearch.Add(invoice.StoreId); - + textSearch.AddRange(invoice.InternalTags); AddToTextSearch(invoice.Id, textSearch.ToArray()); return invoice; } From 078a2d7e392ad5b1443dd74087428b6f7924ffad Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 12:36:09 +0200 Subject: [PATCH 32/63] Reduce Shopify integration steps and make sure checks work. --- BTCPayServer/Controllers/StoresController.Integrations.cs | 7 +++++-- BTCPayServer/Services/Shopify/Models/ShopifySettings.cs | 2 -- BTCPayServer/Services/Shopify/ShopifyApiClient.cs | 2 +- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index e5a84f6f9..ec960e892 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -116,8 +116,6 @@ namespace BTCPayServer.Controllers return View("Integrations", vm); } - shopify.CredentialsValid = true; - var scopesGranted = await apiClient.CheckScopes(); if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_script_tags")) { @@ -125,6 +123,11 @@ namespace BTCPayServer.Controllers "Please grant the private app permissions for read_orders, write_script_tags"; return View("Integrations", vm); } + var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", + new {storeId = CurrentStore.Id}, Request.Scheme)); + + shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); + shopify.IntegratedAt = DateTimeOffset.Now; var blob = CurrentStore.GetStoreBlob(); blob.Shopify = shopify; diff --git a/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs index e7216afcf..59ea917ee 100644 --- a/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs +++ b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs @@ -17,8 +17,6 @@ namespace BTCPayServer.Services.Shopify.Models !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 5da3df3ac..9ba4c412e 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -70,7 +70,7 @@ namespace BTCPayServer.Services.Shopify public async Task CheckScopes() { - var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, null, "admin/oauth/access_scopes.json"); + var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, null, "admin/oauth/access_scopes.json"); return JObject.Parse(await SendRequest(req))["access_scopes"].Values() .Select(token => token["handle"].Value()).ToArray(); } diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index 2c56a5778..ef5bd6157 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -1,7 +1,7 @@ @model IntegrationsViewModel @{ var shopify = Model.Shopify; - var shopifyCredsSet = shopify?.CredentialsValid == true; + var shopifyCredsSet = shopify?.IntegratedAt.HasValue is true; } - - - */ - -! function () { - // extracted from shopify initialized page - const shopify_price = Shopify.checkout.payment_due; - const shopify_currency = Shopify.checkout.currency; - const shopify_order_id = "shopify-" + Shopify.checkout.order_id; - - "use strict"; - const pageElements = document.querySelector.bind(document), - insertElement = (document.querySelectorAll.bind(document), - (e, - n) => { - n.parentNode.insertBefore(e, - n.nextSibling) - }); - - let pageItems = {}, - pageheader = "Thank you!", - buttonElement = null; - - const setPageItems = () => { - pageItems = { - mainHeader: pageElements("#main-header"), - orderConfirmed: pageElements(".os-step__title"), - orderConfirmedDescription: pageElements(".os-step__description"), - continueButton: pageElements(".step__footer__continue-btn"), - checkMarkIcon: pageElements(".os-header__hanging-icon"), - orderStatus: pageElements(".os-header__title"), - paymentMethod: pageElements(".payment-method-list__item__info"), - price: pageElements(".payment-due__price"), - finalPrice: pageElements(".total-recap__final-price"), - orderNumber: pageElements(".os-order-number"), - } - } - - const orderPaid = () => { - pageItems.mainHeader.innerText = pageheader, - pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "block"), - pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "block"), - pageItems.continueButton && (pageItems.continueButton.style.visibility = "visible"), - pageItems.checkMarkIcon && (pageItems.checkMarkIcon.style.visibility = "visible"), - buttonElement && (buttonElement.style.display = "none"); - }; - - window.setOrderAsPaid = orderPaid, - window.openBtcPayShopify = function waitForPaymentMethod() { - if (setPageItems(), "Order canceled" === pageItems.orderStatus.innerText) { - return; - } - - const paymentMethod = pageItems.paymentMethod; - - if (null === paymentMethod) { - return void setTimeout(() => { - waitForPaymentMethod(); - }, 10); - } - - if (-1 === paymentMethod.innerText.toLowerCase().indexOf("bitcoin")) return; - - // If payment method is bitcoin, display instructions and payment button. - pageheader = pageItems.mainHeader.innerText, - pageItems.mainHeader && (pageItems.mainHeader.innerText = "Review and pay!"), - pageItems.continueButton && (pageItems.continueButton.style.visibility = "hidden"), - pageItems.checkMarkIcon && (pageItems.checkMarkIcon.style.visibility = "hidden"), - pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "none"), - pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "none"); - - const url = BTCPAYSERVER_URL + "/invoices" + "?storeId=" + STORE_ID + "&orderId=" + shopify_order_id + "&status=complete"; - - // Check if already paid. - fetch(url, { - method: "GET", - mode: "cors", // no-cors, cors, *same-origin, - headers: { - "Content-Type": "application/json", - "accept": "application/json", - }, - }) - .then(function (response) { - return response.json(); - }) - .then(function (json) { - return json.data; - }) - .then(function (data) { - if (data.length != 0) { - orderPaid(); - } - }); - - window.waitForPayment = function () { - buttonElement.innerHTML = "Displaying Invoice..."; - BtcPayServerModal.show( - BTCPAYSERVER_URL, - STORE_ID, - { - price: shopify_price, - currency: shopify_currency, - orderId: shopify_order_id - } - ) - .then(function (invoice) { - buttonElement.innerHTML = payButtonHtml; - if (invoice != null) { - orderPaid(); - } - }); - } - - // Payment button that opens modal - const payButtonHtml = ''; - - buttonElement = document.createElement("div"); - buttonElement.innerHTML = payButtonHtml; - insertElement(buttonElement, pageItems.orderConfirmed); - - } - - window.openBtcPayShopify(); -}(); diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js new file mode 100644 index 000000000..c2c1e28ea --- /dev/null +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -0,0 +1,150 @@ +window.BTCPayShopifyIntegrationModule = function () { + if (!window.btcpay) { + throw new Error("The BTCPay modal js was not loaded on this page."); + } + if (!window.Shopify) { + throw new Error("The Shopify global object was not loaded on this page."); + } + if (!window.BTCPAYSERVER_URL || !window.STORE_ID) { + throw new Error("The BTCPAYSERVER_URL STORE_ID global vars were not set on this page."); + } + const shopify_order_id = Shopify.checkout.order_id; + const btcPayServerUrl = window.BTCPAYSERVER_URL; + const storeId = window.STORE_ID; + var currentInvoiceData; + var modalShown = false; + + const pageElements = document.querySelector.bind(document); + const insertElement = (document.querySelectorAll.bind(document), + (e, + n) => { + n.parentNode.insertBefore(e, + n.nextSibling) + }); + + let buttonElement = null; + + const pageItems = { + mainHeader: pageElements("#main-header"), + orderConfirmed: pageElements(".os-step__title"), + orderConfirmedDescription: pageElements(".os-step__description"), + continueButton: pageElements(".step__footer__continue-btn"), + checkMarkIcon: pageElements(".os-header__hanging-icon"), + orderStatus: pageElements(".os-header__title"), + paymentMethod: pageElements(".payment-method-list__item__info"), + price: pageElements(".payment-due__price"), + finalPrice: pageElements(".total-recap__final-price"), + orderNumber: pageElements(".os-order-number"), + } + + function setOrderAsPaid() { + pageItems.mainHeader.innerText = "Thank you!", + pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "block"), + pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "block"), + pageItems.continueButton && (pageItems.continueButton.style.visibility = "visible"), + pageItems.checkMarkIcon && (pageItems.checkMarkIcon.style.visibility = "visible"), + buttonElement && (buttonElement.style.display = "none"); + } + + function showPaymentInstructions() { + pageItems.mainHeader && (pageItems.mainHeader.innerText = "Review and pay!"), + pageItems.continueButton && (pageItems.continueButton.style.visibility = "hidden"), + pageItems.checkMarkIcon && (pageItems.checkMarkIcon.style.visibility = "hidden"), + pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "none"), + pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "none"); + } + + function getOrCheckInvoice(backgroundCheck) { + const url = btcPayServerUrl + "/stores/" + storeId + "/integrations/shopify/" + shopify_order_id + (backgroundCheck ? "?checkonly=true" : ""); + return fetch(url, { + method: "GET", + mode: "cors", // no-cors, cors, *same-origin, + headers: { + "Content-Type": "application/json", + "accept": "application/json", + } + }) + .then(function (response) { + return response.json(); + }).catch(function () { + if (!backgroundCheck) + alert("Could not initiate BTCPay Server payment method, try again later."); + }) + } + + function onPayButtonClicked() { + buttonElement.innerHTML = "Displaying Invoice..."; + + getOrCheckInvoice().then(handleInvoiceData).catch(fail.bind(this)); + } + + function handleInvoiceData(data, opts) { + currentInvoiceData = data; + if (!currentInvoiceData) { + window.btcpay.hideFrame(); + return; + } + if (["complete", "confirmed", "paid"].indexOf(currentInvoiceData.status.toLowerCase()) >= 0) { + setOrderAsPaid(); + } else if (["invalid", "expired"].indexOf(currentInvoiceData.status.toLowerCase()) >= 0) { + fail(); + } else if (!opts || !opts.backgroundCheck) { + showModal(); + } + } + + function showModal() { + if (currentInvoiceData && !modalShown) { + modalShown = true; + window.btcpay.setApiUrlPrefix(btcPayServerUrl); + + window.btcpay.onModalReceiveMessage(function (evt) { + if (evt && evt.invoiceId && evt.status) { + currentInvoiceData = evt; + } + }); + + window.btcpay.onModalWillEnter(function () { + modalShown = true; + }); + + window.btcpay.onModalWillLeave(function () { + modalShown = false; + fail(); + }); + window.btcpay.showInvoice(currentInvoiceData.invoiceId); + } + } + + function fail() { + currentInvoiceData = null; + buttonElement.innerHTML = payButtonHtml; + } + + const payButtonHtml = ''; + + function injectPaymentButtonHtml() { + // Payment button that opens modal + buttonElement = document.getElementById("btcpayserver-pay"); + if (buttonElement) { + return; + } + buttonElement = document.createElement("div"); + buttonElement.id = "btcpayserver-pay"; + buttonElement.innerHTML = payButtonHtml; + insertElement(buttonElement, pageItems.orderConfirmed); + } + + if (["bitcoin", "btc", "btcpayserver"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { + return; + } + showPaymentInstructions(); + injectPaymentButtonHtml(); + window.onPayButtonClicked = onPayButtonClicked.bind(this); + getOrCheckInvoice(true).then(function (d) { + handleInvoiceData(d, {backgroundCheck: true}) + }); + +}; + +window.addEventListener("load", BTCPayShopifyIntegrationModule); From c9f02955168697d514f09c0002beda4b34804885 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 21 Sep 2020 15:34:41 +0200 Subject: [PATCH 34/63] Make Shopify transaction register system handle marked payments slightly more --- .../Shopify/OrderTransactionRegisterLogic.cs | 84 ++++++++++++------- .../wwwroot/shopify/btcpay-shopify.js | 16 +++- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 4735f9bac..23e391d49 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -20,40 +20,62 @@ namespace BTCPayServer.Services.Shopify { currency = currency.ToUpperInvariant().Trim(); var existingShopifyOrderTransactions = (await _client.TransactionsList(orderId)).transactions; - // only register transactions if first, parent_id transaction is present and we haven't already registered transaction for this invoice( or if there was one registered but it was a failure and this one is success, in the case of a merchant marking it as complete) - if (existingShopifyOrderTransactions != null && existingShopifyOrderTransactions.Count >= 1 && existingShopifyOrderTransactions.All(a => a.authorization != invoiceId || (!success || a.status == "failure"))) + + if (existingShopifyOrderTransactions?.Count < 1) { - var transaction = existingShopifyOrderTransactions[0]; - - if (currency.ToUpperInvariant().Trim() != transaction.currency.ToUpperInvariant().Trim()) - { - // because of parent_id present, currency will always be the one from parent transaction - // malicious attacker could potentially exploit this by creating invoice - // in different currency and paying that one, registering order on Shopify as paid - // so if currency is supplied and is different from parent transaction currency we just won't register - return null; - } - - var createTransaction = new TransactionsCreateReq - { - transaction = new TransactionsCreateReq.DataHolder - { - parent_id = transaction.id, - currency = currency, - amount = amountCaptured, - kind = "capture", - gateway = "BTCPayServer", - source = "external", - authorization = invoiceId, - status = success? "success": "failure" - } - }; - - var createResp = await _client.TransactionCreate(orderId, createTransaction); - return createResp; + return null; + } + + + var baseParentTransaction = existingShopifyOrderTransactions[0]; + + if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim()) + { + // because of parent_id present, currency will always be the one from parent transaction + // malicious attacker could potentially exploit this by creating invoice + // in different currency and paying that one, registering order on Shopify as paid + // so if currency is supplied and is different from parent transaction currency we just won't register + return null; } - return null; + var kind = "capture"; + var parentId = baseParentTransaction.id; + var status = success ? "success" : "failure"; + var existingShopifyOrderTransactionsOnSameInvoice = + existingShopifyOrderTransactions.Where(holder => holder.authorization == invoiceId); + + var successfulActions = + existingShopifyOrderTransactionsOnSameInvoice.Where(holder => holder.status == "success").ToArray(); + + var successfulCaptures = successfulActions.Where(holder => holder.kind == "capture").ToArray(); + var refunds = successfulActions.Where(holder => holder.kind == "refund").ToArray(); + + if (!success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0) + { + kind = "void"; + parentId = successfulCaptures.Last().id; + status = "success"; + } + else if(success && successfulCaptures.Length >0 && (successfulCaptures.Length - refunds.Length ) > 0 ) + { + return null; + } + var createTransaction = new TransactionsCreateReq + { + transaction = new TransactionsCreateReq.DataHolder + { + parent_id = parentId, + currency = currency, + amount = amountCaptured, + kind = kind, + gateway = "BTCPayServer", + source = "external", + authorization = invoiceId, + status = status + } + }; + var createResp = await _client.TransactionCreate(orderId, createTransaction); + return createResp; } } } diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index c2c1e28ea..d9cdaf1e0 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -81,7 +81,14 @@ window.BTCPayShopifyIntegrationModule = function () { function handleInvoiceData(data, opts) { currentInvoiceData = data; if (!currentInvoiceData) { - window.btcpay.hideFrame(); + if (modalShown) { + window.btcpay.hideFrame(); + fail(); + }else if(opts && opts.backgroundCheck){ + injectPaymentButtonHtml(); + }else{ + fail(); + } return; } if (["complete", "confirmed", "paid"].indexOf(currentInvoiceData.status.toLowerCase()) >= 0) { @@ -90,7 +97,7 @@ window.BTCPayShopifyIntegrationModule = function () { fail(); } else if (!opts || !opts.backgroundCheck) { showModal(); - } + } } function showModal() { @@ -110,7 +117,9 @@ window.BTCPayShopifyIntegrationModule = function () { window.btcpay.onModalWillLeave(function () { modalShown = false; - fail(); + getOrCheckInvoice(true).then(function (d) { + handleInvoiceData(d, {backgroundCheck: true}) + }); }); window.btcpay.showInvoice(currentInvoiceData.invoiceId); } @@ -139,7 +148,6 @@ window.BTCPayShopifyIntegrationModule = function () { return; } showPaymentInstructions(); - injectPaymentButtonHtml(); window.onPayButtonClicked = onPayButtonClicked.bind(this); getOrCheckInvoice(true).then(function (d) { handleInvoiceData(d, {backgroundCheck: true}) From e9bd1bad67cc4a1c5668e7794d38a31f49c9163f Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 23 Sep 2020 00:04:48 -0500 Subject: [PATCH 35/63] Removing reference to Nethereum, Bitcoin only build --- BTCPayServer/Controllers/StoresController.Integrations.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index c136d869a..cb3b10782 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -15,7 +15,6 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; -using Nethereum.Util; using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers @@ -85,7 +84,7 @@ namespace BTCPayServer.Controllers return Ok(new { invoiceId = firstInvoiceStillPending.Id, - status = firstInvoiceStillPending.Status.ToStringInvariant().ToLowerInvariant() + status = firstInvoiceStillPending.Status.ToString().ToLowerInvariant() }); } @@ -97,7 +96,7 @@ namespace BTCPayServer.Controllers return Ok(new { invoiceId = firstInvoiceSettled.Id, - status = firstInvoiceSettled.Status.ToStringInvariant().ToLowerInvariant() + status = firstInvoiceSettled.Status.ToString().ToLowerInvariant() }); } @@ -123,7 +122,7 @@ namespace BTCPayServer.Controllers return Ok(new { invoiceId = invoice.Id, - status = invoice.Status.ToStringInvariant().ToLowerInvariant() + status = invoice.Status.ToString().ToLowerInvariant() }); } return NotFound(); From 03d6c8689956a73e58e495fa4788dd399ff6c0e2 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 24 Sep 2020 17:33:15 +0200 Subject: [PATCH 36/63] Revert "Make Invoices save internal tags in the text search" This reverts commit f352399428f5aacfaa237061488771f023c7f7fd. --- BTCPayServer/Services/Invoices/InvoiceRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 4c6ec3331..8c40e40f7 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -204,7 +204,7 @@ retry: textSearch.Add(invoice.Metadata.OrderId); textSearch.Add(ToString(invoice.Metadata, null)); textSearch.Add(invoice.StoreId); - textSearch.AddRange(invoice.InternalTags); + AddToTextSearch(invoice.Id, textSearch.ToArray()); return invoice; } From a7d7b5abc33092949be205f1da3746ccef4f37af Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 24 Sep 2020 18:18:17 +0200 Subject: [PATCH 37/63] set order id of shopify invoice and serch by order id + storeId --- BTCPayServer/Controllers/StoresController.Integrations.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index cb3b10782..12d991d99 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -15,6 +15,7 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers @@ -55,7 +56,6 @@ namespace BTCPayServer.Controllers return Content(jsFile, "text/javascript"); } - [RateLimitsFilter(ZoneLimits.Shopify, Scope = RateLimitsScope.RemoteAddress)] [AllowAnonymous] [EnableCors(CorsPolicies.All)] @@ -70,7 +70,7 @@ namespace BTCPayServer.Controllers var invoiceOrderId = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}"; var matchedExistingInvoices = await invoiceRepository.GetInvoices(new InvoiceQuery() { - TextSearch = invoiceOrderId + OrderId = new[] {invoiceOrderId}, StoreId = new[] {storeId} }); matchedExistingInvoices = matchedExistingInvoices.Where(entity => entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX) @@ -116,7 +116,7 @@ namespace BTCPayServer.Controllers } var invoice = await invoiceController.CreateInvoiceCoreRaw( - new CreateInvoiceRequest() {Amount = order.TotalPrice, Currency = order.Currency}, store, + new CreateInvoiceRequest() {Amount = order.TotalPrice, Currency = order.Currency, Metadata = new JObject {["orderId"] = invoiceOrderId} }, store, Request.GetAbsoluteUri(""), new List() {invoiceOrderId}); return Ok(new From 47e9f820e48fcef9f18c2d5ea546183c0fc87f7b Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 13:50:04 -0500 Subject: [PATCH 38/63] Switching the flow to exclusively use Example Url --- .../StoresController.Integrations.cs | 2 +- .../Views/Stores/Integrations/Shopify.cshtml | 85 +++++++++---------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 12d991d99..1148ff7f4 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -164,7 +164,7 @@ namespace BTCPayServer.Controllers } catch (Exception) { - TempData[WellKnownTempData.ErrorMessage] = "The provided example url was invalid."; + TempData[WellKnownTempData.ErrorMessage] = "The provided Example Url was invalid."; return View("Integrations", vm); } } diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index ef5bd6157..6b66ff9f5 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -3,15 +3,6 @@ var shopify = Model.Shopify; var shopifyCredsSet = shopify?.IntegratedAt.HasValue is true; } -

Shopify @@ -21,48 +12,50 @@ function promptExampleUrl(){

@if (!shopifyCredsSet) { -

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

- - } -
- -
- @if (!Model.Shopify?.ShopName?.Contains(".") is true) - { -
- https:// -
- } - +

Create a Shopify Private App with the permissions "Script tags - Read and write" and "Orders - Read and write"

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

Orders on @shopify.ShopName.myshopify.com will be marked as paid on successful invoice payment. Started: @shopify.IntegratedAt.Value.ToBrowserDate() @@ -86,10 +79,8 @@ function promptExampleUrl(){

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

- } - @if (shopifyCredsSet) - { + From 694cedb89ac2dfe370ab8639ab79b42ad3335406 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 13:50:23 -0500 Subject: [PATCH 39/63] Handling scenario where user provides invalid Shopify credentials --- .../Controllers/StoresController.Integrations.cs | 2 +- BTCPayServer/Services/Shopify/ShopifyApiClient.cs | 5 ++++- .../Services/Shopify/ShopifyApiException.cs | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer/Services/Shopify/ShopifyApiException.cs diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 1148ff7f4..38eeafb2f 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -185,7 +185,7 @@ namespace BTCPayServer.Controllers { await apiClient.OrdersCount(); } - catch + catch (ShopifyApiException) { TempData[WellKnownTempData.ErrorMessage] = "Shopify rejected provided credentials, please correct values and try again"; diff --git a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs index c4156f546..8f9ce4d9b 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -29,7 +29,7 @@ namespace BTCPayServer.Services.Shopify _credentials = credentials; - var bearer = $"{credentials.ApiKey}:{credentials.ApiPassword}"; + var bearer = $"{_credentials.ApiKey}:{_credentials.ApiPassword}"; bearer = Encoding.UTF8.GetBytes(bearer).ToBase64String(); _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + bearer); @@ -49,6 +49,9 @@ namespace BTCPayServer.Services.Shopify using var resp = await _httpClient.SendAsync(req); var strResp = await resp.Content.ReadAsStringAsync(); + if (strResp?.StartsWith("{\"errors\":\"[API] Invalid API key or access token", StringComparison.OrdinalIgnoreCase) == true) + throw new ShopifyApiException("Invalid API key or access token"); + return strResp; } diff --git a/BTCPayServer/Services/Shopify/ShopifyApiException.cs b/BTCPayServer/Services/Shopify/ShopifyApiException.cs new file mode 100644 index 000000000..28ca4c3ee --- /dev/null +++ b/BTCPayServer/Services/Shopify/ShopifyApiException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Shopify +{ + public class ShopifyApiException : Exception + { + public ShopifyApiException(string message) : base(message) + { + } + } +} From 48acd101a55b212137cabf71bf4329e5f46acd70 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 13:55:34 -0500 Subject: [PATCH 40/63] Handling errors with registering store script --- .../StoresController.Integrations.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 38eeafb2f..9cb2f3a80 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -199,10 +199,21 @@ namespace BTCPayServer.Controllers "Please grant the private app permissions for read_orders, write_script_tags"; return View("Integrations", vm); } - var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", - new {storeId = CurrentStore.Id}, Request.Scheme)); - shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); + try + { + var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", + new { storeId = CurrentStore.Id }, Request.Scheme)); + + shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); + } + catch + { + // ignore errors, signify ScriptId needs to be set manually + shopify.ScriptId = null; + } + + // everything ready, proceed with saving Shopify integration credentials shopify.IntegratedAt = DateTimeOffset.Now; var blob = CurrentStore.GetStoreBlob(); From e955ddb9f13c63fb4d1f0fe0b41f5e7c2b682e55 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 13:58:52 -0500 Subject: [PATCH 41/63] Changing button text to better signal operation --- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index 6b66ff9f5..6b0a54b39 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -19,7 +19,7 @@
- + } else { From eb6b1b431cca221b01a481def5c29b6a5fd1c753 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 14:13:57 -0500 Subject: [PATCH 42/63] Removing duplicate FromServices import --- BTCPayServer/Controllers/StoresController.Integrations.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 9cb2f3a80..cdeb377f8 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -61,7 +61,6 @@ namespace BTCPayServer.Controllers [EnableCors(CorsPolicies.All)] [HttpGet("{storeId}/integrations/shopify/{orderId}")] public async Task ShopifyInvoiceEndpoint( - [FromServices] StoreRepository storeRepository, [FromServices] InvoiceRepository invoiceRepository, [FromServices] InvoiceController invoiceController, [FromServices] IHttpClientFactory httpClientFactory, @@ -104,7 +103,7 @@ namespace BTCPayServer.Controllers { return Ok(); } - var store = await storeRepository.FindStore(storeId); + var store = await _Repo.FindStore(storeId); var shopify = store?.GetStoreBlob()?.Shopify; if (shopify?.IntegratedAt.HasValue is true) { From b4f0cf510d1d36c95333e371de4a0cdf5916e51a Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 16:06:29 -0500 Subject: [PATCH 43/63] Ensuring that elements on page are refreshed as soon as possible Waiting on `load` is way too long, results in bad UX --- .../wwwroot/shopify/btcpay-shopify.js | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index d9cdaf1e0..ec5f47a78 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -1,4 +1,20 @@ window.BTCPayShopifyIntegrationModule = function () { + const pageElements = document.querySelector.bind(document); + const insertElement = (document.querySelectorAll.bind(document), + (e, + n) => { + n.parentNode.insertBefore(e, + n.nextSibling) + }); + + // execute BTCPayShopifyIntegrationModule as soon as possible + var paymentMethod = pageElements(".payment-method-list__item__info"); + if (null === paymentMethod) { + return void setTimeout(() => { + window.BTCPayShopifyIntegrationModule(); + }, 10); + } + if (!window.btcpay) { throw new Error("The BTCPay modal js was not loaded on this page."); } @@ -14,17 +30,9 @@ window.BTCPayShopifyIntegrationModule = function () { var currentInvoiceData; var modalShown = false; - const pageElements = document.querySelector.bind(document); - const insertElement = (document.querySelectorAll.bind(document), - (e, - n) => { - n.parentNode.insertBefore(e, - n.nextSibling) - }); - let buttonElement = null; - const pageItems = { + var pageItems = { mainHeader: pageElements("#main-header"), orderConfirmed: pageElements(".os-step__title"), orderConfirmedDescription: pageElements(".os-step__description"), @@ -35,7 +43,7 @@ window.BTCPayShopifyIntegrationModule = function () { price: pageElements(".payment-due__price"), finalPrice: pageElements(".total-recap__final-price"), orderNumber: pageElements(".os-order-number"), - } + }; function setOrderAsPaid() { pageItems.mainHeader.innerText = "Thank you!", @@ -155,4 +163,4 @@ window.BTCPayShopifyIntegrationModule = function () { }; -window.addEventListener("load", BTCPayShopifyIntegrationModule); +window.BTCPayShopifyIntegrationModule(); From 02b99d2b06e490d78ece3d19ac92a4d75e3a8a39 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 16:55:28 -0500 Subject: [PATCH 44/63] Injecting payment button, wasn't displaying when invoice already present Also button was not getting refresh on modal close --- BTCPayServer/wwwroot/shopify/btcpay-shopify.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index ec5f47a78..2aab348e6 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -126,6 +126,7 @@ window.BTCPayShopifyIntegrationModule = function () { window.btcpay.onModalWillLeave(function () { modalShown = false; getOrCheckInvoice(true).then(function (d) { + buttonElement.innerHTML = payButtonHtml; handleInvoiceData(d, {backgroundCheck: true}) }); }); @@ -158,6 +159,7 @@ window.BTCPayShopifyIntegrationModule = function () { showPaymentInstructions(); window.onPayButtonClicked = onPayButtonClicked.bind(this); getOrCheckInvoice(true).then(function (d) { + injectPaymentButtonHtml(); handleInvoiceData(d, {backgroundCheck: true}) }); From d33a449332a3465c284b20c74bcbaac57b901042 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 25 Sep 2020 16:55:42 -0500 Subject: [PATCH 45/63] Changing page title during before and after payment --- BTCPayServer/wwwroot/shopify/btcpay-shopify.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index 2aab348e6..74e9a7431 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -46,6 +46,7 @@ window.BTCPayShopifyIntegrationModule = function () { }; function setOrderAsPaid() { + document.title = document.title.replace("Review and pay!", "Thank you!"); pageItems.mainHeader.innerText = "Thank you!", pageItems.orderConfirmed && (pageItems.orderConfirmed.style.display = "block"), pageItems.orderConfirmedDescription && (pageItems.orderConfirmedDescription.style.display = "block"), @@ -55,6 +56,7 @@ window.BTCPayShopifyIntegrationModule = function () { } function showPaymentInstructions() { + document.title = document.title.replace("Thank you!", "Review and pay!"); pageItems.mainHeader && (pageItems.mainHeader.innerText = "Review and pay!"), pageItems.continueButton && (pageItems.continueButton.style.visibility = "hidden"), pageItems.checkMarkIcon && (pageItems.checkMarkIcon.style.visibility = "hidden"), From 081259692950d265e0c1d9342ec7b4e50e0bd26d Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 26 Sep 2020 12:22:16 -0500 Subject: [PATCH 46/63] Adding another string for integration check --- BTCPayServer/wwwroot/shopify/btcpay-shopify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index 74e9a7431..3e8c7c507 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -155,7 +155,7 @@ window.BTCPayShopifyIntegrationModule = function () { insertElement(buttonElement, pageItems.orderConfirmed); } - if (["bitcoin", "btc", "btcpayserver"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { + if (["bitcoin", "btc", "btcpayserver", "btcpay server"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { return; } showPaymentInstructions(); From c14fd69a0ee7a7677e9290c37de5291a05e80464 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 26 Sep 2020 12:51:29 -0500 Subject: [PATCH 47/63] Disabling automatic creation of Script in Shopify --- .../StoresController.Integrations.cs | 27 ++++++++++--------- .../Views/Stores/Integrations/Shopify.cshtml | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index cdeb377f8..8e8527d01 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -192,25 +192,26 @@ namespace BTCPayServer.Controllers } var scopesGranted = await apiClient.CheckScopes(); - if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_script_tags")) + if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_orders")) { TempData[WellKnownTempData.ErrorMessage] = - "Please grant the private app permissions for read_orders, write_script_tags"; + "Please grant the private app permissions for read_orders, write_orders"; return View("Integrations", vm); } - try - { - var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", - new { storeId = CurrentStore.Id }, Request.Scheme)); + // Not automatically registering scripts + //try + //{ + // var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", + // new { storeId = CurrentStore.Id }, Request.Scheme)); - shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); - } - catch - { - // ignore errors, signify ScriptId needs to be set manually - shopify.ScriptId = null; - } + // shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); + //} + //catch + //{ + // // ignore errors, signify ScriptId needs to be set manually + // shopify.ScriptId = null; + //} // everything ready, proceed with saving Shopify integration credentials shopify.IntegratedAt = DateTimeOffset.Now; diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index 6b0a54b39..f67eeb545 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -12,7 +12,7 @@

@if (!shopifyCredsSet) { -

Create a Shopify Private App with the permissions "Script tags - Read and write" and "Orders - Read and write"

+

Create a Shopify Private App with the permissions "Orders - Read and write"

From c22536ce67f1435452d0fd82fe302ad15401834b Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 26 Sep 2020 12:55:26 -0500 Subject: [PATCH 48/63] Tweaking display order and style of alerts --- .../Views/Stores/Integrations/Shopify.cshtml | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index f67eeb545..7a3804db5 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -55,29 +55,28 @@
- -

- 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 + In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts

- - @($"") - + + <script src='https://btcpay151676.lndyn.com/stores/4mWitQRd4pdw4SRai6xRZnZB3Y4LbR953duK1kK58Hgh/integrations/shopify/shopify.js'></script> +
} 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 +

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

+ +

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

From 84f9ee3765d7d02b0b48b37a724a1fe94e31a3ab Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 26 Sep 2020 13:17:11 -0500 Subject: [PATCH 49/63] Word breaking on all chars for better display of additional script --- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index 7a3804db5..1b4764f19 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -61,7 +61,7 @@

In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts

- + <script src='https://btcpay151676.lndyn.com/stores/4mWitQRd4pdw4SRai6xRZnZB3Y4LbR953duK1kK58Hgh/integrations/shopify/shopify.js'></script>
From bf7c8d5a5c6d0c69014dd0d9b6f088645bdb23cd Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 26 Sep 2020 13:27:18 -0500 Subject: [PATCH 50/63] Bugfixing references to shop, correcting them to be dynamic --- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index 1b4764f19..e580286cc 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -59,10 +59,10 @@ {

- In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts + In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts

- <script src='https://btcpay151676.lndyn.com/stores/4mWitQRd4pdw4SRai6xRZnZB3Y4LbR953duK1kK58Hgh/integrations/shopify/shopify.js'></script> + @($"")
} @@ -71,7 +71,7 @@

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

}

- In Shopify please add a payment method at Settings > Payments > Manual Payment Methods with the name Bitcoin with BTCPay Server + In Shopify please add a payment method at Settings > Payments > Manual Payment Methods with the name Bitcoin with BTCPay Server

From 4a596c737335d1ef046c65be97c403c85884c52a Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 07:58:49 +0200 Subject: [PATCH 51/63] Remove leftover shopify API calls and script creation --- .../StoresController.Integrations.cs | 33 +------------------ .../Shopify/Models/ShopifySettings.cs | 1 - .../Services/Shopify/ShopifyApiClient.cs | 29 ---------------- 3 files changed, 1 insertion(+), 62 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 8e8527d01..c788c5b8e 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -192,6 +192,7 @@ namespace BTCPayServer.Controllers } var scopesGranted = await apiClient.CheckScopes(); + //TODO: check if these are actually needed if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_orders")) { TempData[WellKnownTempData.ErrorMessage] = @@ -199,20 +200,6 @@ namespace BTCPayServer.Controllers return View("Integrations", vm); } - // Not automatically registering scripts - //try - //{ - // var result = await apiClient.CreateScript(Url.Action("ShopifyJavascript", "Stores", - // new { storeId = CurrentStore.Id }, Request.Scheme)); - - // shopify.ScriptId = result.ScriptTag?.Id.ToString(CultureInfo.InvariantCulture); - //} - //catch - //{ - // // ignore errors, signify ScriptId needs to be set manually - // shopify.ScriptId = null; - //} - // everything ready, proceed with saving Shopify integration credentials shopify.IntegratedAt = DateTimeOffset.Now; @@ -229,24 +216,6 @@ namespace BTCPayServer.Controllers 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)) { diff --git a/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs index 59ea917ee..f138a26f1 100644 --- a/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs +++ b/BTCPayServer/Services/Shopify/Models/ShopifySettings.cs @@ -18,6 +18,5 @@ namespace BTCPayServer.Services.Shopify.Models !string.IsNullOrWhiteSpace(Password); } 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 8f9ce4d9b..4d1accd7d 100644 --- a/BTCPayServer/Services/Shopify/ShopifyApiClient.cs +++ b/BTCPayServer/Services/Shopify/ShopifyApiClient.cs @@ -78,25 +78,6 @@ namespace BTCPayServer.Services.Shopify .Select(token => token["handle"].Value()).ToArray(); } - 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(_credentials.ShopName, HttpMethod.Get, $"orders/{orderId}/transactions.json"); @@ -119,16 +100,6 @@ namespace BTCPayServer.Services.Shopify return JsonConvert.DeserializeObject(strResp); } - public async Task UpdateOrderNote(string orderId, string note) - { - var postJson = JsonConvert.SerializeObject(new {order = new {id = orderId, note}}); - - var req = CreateRequest(_credentials.ShopName, HttpMethod.Put, $"orders/{orderId}.json"); - req.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); - - await SendRequest(req); - } - public async Task GetOrder(string orderId) { var req = CreateRequest(_credentials.ShopName, HttpMethod.Get, From c07952629cabb86a4ff8bd5d5976bdc73a4107ca Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 08:30:50 +0200 Subject: [PATCH 52/63] Commenting OrderTransactionRegisterLogic --- .../Shopify/OrderTransactionRegisterLogic.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 23e391d49..77f4f1d8f 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -1,9 +1,6 @@ -using System; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Services.Shopify.ApiModels; -using Microsoft.EntityFrameworkCore.Internal; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Shopify { @@ -25,10 +22,10 @@ namespace BTCPayServer.Services.Shopify { return null; } - - + //TODO: verify if we should be doing this or filtering out the parent transaction by the gateway (the one that shows in the checkout UI, aka the manual payment method created by the merchant) var baseParentTransaction = existingShopifyOrderTransactions[0]; + //technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint. if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim()) { // because of parent_id present, currency will always be the one from parent transaction @@ -41,21 +38,28 @@ namespace BTCPayServer.Services.Shopify var kind = "capture"; var parentId = baseParentTransaction.id; var status = success ? "success" : "failure"; + //find all existing transactions recorded around this invoice id var existingShopifyOrderTransactionsOnSameInvoice = existingShopifyOrderTransactions.Where(holder => holder.authorization == invoiceId); + //filter out the successful ones var successfulActions = existingShopifyOrderTransactionsOnSameInvoice.Where(holder => holder.status == "success").ToArray(); + //of the successful ones, get the ones we registered as a valid payment var successfulCaptures = successfulActions.Where(holder => holder.kind == "capture").ToArray(); + + //of the successful ones, get the ones we registered as a voiding of a previous successful payment var refunds = successfulActions.Where(holder => holder.kind == "refund").ToArray(); + //if we are working with a non-success registration, but see that we have previously registered this invoice as a success, we switch to creating a "void" transaction, which in shopify terms is a refund. if (!success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0) { kind = "void"; parentId = successfulCaptures.Last().id; status = "success"; } + //if we are working with a success registration, but can see that we have already had a successful transaction saved, get outta here else if(success && successfulCaptures.Length >0 && (successfulCaptures.Length - refunds.Length ) > 0 ) { return null; From 713a1f03e91c69c0d296c3ae1f1b4f90a4527480 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 08:41:55 +0200 Subject: [PATCH 53/63] Fix warnings --- BTCPayServer/HostedServices/InvoiceNotificationManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 37d3048c1..eb8fefee4 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -331,17 +331,17 @@ namespace BTCPayServer.HostedServices e.Name == InvoiceEvent.Completed || e.Name == InvoiceEvent.ExpiredPaidPartial ) - Notify(invoice, e, false); + _ = Notify(invoice, e, false); } if (e.Name == InvoiceEvent.Confirmed) { - Notify(invoice, e, false); + _ = Notify(invoice, e, false); } if (invoice.ExtendedNotifications) { - Notify(invoice, e, true); + _ = Notify(invoice, e, true); } })); From dc0cf26f516586603da1c8d4167d81e1e1935d6b Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 08:42:03 +0200 Subject: [PATCH 54/63] Fix build --- .../Views/Stores/Integrations/Shopify.cshtml | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index e580286cc..a93e99bc1 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -2,6 +2,7 @@ @{ var shopify = Model.Shopify; var shopifyCredsSet = shopify?.IntegratedAt.HasValue is true; + var shopifyUrl = !Model.Shopify?.ShopName?.Contains(".") is true ? $"https://{shopify.ShopName}.myshopify.com" : shopify.ShopName; }

@@ -55,23 +56,16 @@

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

- In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts -

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

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

- } +
+

+ In Shopify please paste following script at Settings > Checkout > Order Processing > Additional Scripts +

+ + @($"") + +

- In Shopify please add a payment method at Settings > Payments > Manual Payment Methods with the name Bitcoin with BTCPay Server + In Shopify please add a payment method at Settings > Payments > Manual Payment Methods with the name Bitcoin with BTCPay Server

From 3ce626921241c9327b0a592b5a07fb28966cbe6d Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 08:42:14 +0200 Subject: [PATCH 55/63] Remove unused imports --- BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 77f4f1d8f..5512fe756 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -1,6 +1,9 @@ +using System; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Services.Shopify.ApiModels; +using Microsoft.EntityFrameworkCore.Internal; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Shopify { From 4ffd2265ea22422be5fd25088df234a7109171d6 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 09:09:28 +0200 Subject: [PATCH 56/63] Reduce unused branching in UI --- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index a93e99bc1..dc33535f4 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -73,9 +73,8 @@ Started: @shopify.IntegratedAt.Value.ToBrowserDate()

- } From 0a726d598cebda3afa03cf511582ffb03bf7323c Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 10:37:35 +0200 Subject: [PATCH 57/63] Make sure shopify var is set in the UI --- BTCPayServer/Views/Stores/Integrations/Shopify.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml index dc33535f4..a45b4047a 100644 --- a/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml +++ b/BTCPayServer/Views/Stores/Integrations/Shopify.cshtml @@ -2,7 +2,7 @@ @{ var shopify = Model.Shopify; var shopifyCredsSet = shopify?.IntegratedAt.HasValue is true; - var shopifyUrl = !Model.Shopify?.ShopName?.Contains(".") is true ? $"https://{shopify.ShopName}.myshopify.com" : shopify.ShopName; + var shopifyUrl = shopify is null? null: !shopify?.ShopName?.Contains(".") is true ? $"https://{shopify.ShopName}.myshopify.com" : shopify.ShopName; }

From 6da6723c72010f43bcc61da82da691e4ea1b6e9a Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 10:39:00 +0200 Subject: [PATCH 58/63] Be sure to match transaction gateway in Shopify registration --- .../Shopify/OrderTransactionRegisterLogic.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 5512fe756..a43fc730d 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Services.Shopify.ApiModels; -using Microsoft.EntityFrameworkCore.Internal; -using Newtonsoft.Json.Linq; +using BTCPayServer.Services.Shopify.ApiModels.DataHolders; namespace BTCPayServer.Services.Shopify { @@ -16,17 +16,21 @@ namespace BTCPayServer.Services.Shopify _client = client; } + private static TransactionDataHolder GetParentTransaction(List txs) + { + var keywords = new[] {"bitcoin", "btc"}; + return txs.FirstOrDefault(holder =>keywords .Any(s => holder.gateway.Contains(s, StringComparison.InvariantCultureIgnoreCase))); + } + public async Task Process(string orderId, string invoiceId, string currency, string amountCaptured, bool success) { currency = currency.ToUpperInvariant().Trim(); var existingShopifyOrderTransactions = (await _client.TransactionsList(orderId)).transactions; - - if (existingShopifyOrderTransactions?.Count < 1) + var baseParentTransaction = GetParentTransaction(existingShopifyOrderTransactions); + if (baseParentTransaction is null) { return null; } - //TODO: verify if we should be doing this or filtering out the parent transaction by the gateway (the one that shows in the checkout UI, aka the manual payment method created by the merchant) - var baseParentTransaction = existingShopifyOrderTransactions[0]; //technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint. if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim()) From 8f8562705a75f3ea8de0e474cab843e8aa94f47d Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 10:39:26 +0200 Subject: [PATCH 59/63] Pass due amount from shopify UI in case of partial payments --- BTCPayServer/wwwroot/shopify/btcpay-shopify.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index 3e8c7c507..1a2ff14ef 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -65,7 +65,7 @@ window.BTCPayShopifyIntegrationModule = function () { } function getOrCheckInvoice(backgroundCheck) { - const url = btcPayServerUrl + "/stores/" + storeId + "/integrations/shopify/" + shopify_order_id + (backgroundCheck ? "?checkonly=true" : ""); + const url = btcPayServerUrl + "/stores/" + storeId + "/integrations/shopify/" + shopify_order_id+"?amount="+Shopify.checkout.payment_due+ (backgroundCheck ? "&checkonly=true" : ""); return fetch(url, { method: "GET", mode: "cors", // no-cors, cors, *same-origin, @@ -155,7 +155,7 @@ window.BTCPayShopifyIntegrationModule = function () { insertElement(buttonElement, pageItems.orderConfirmed); } - if (["bitcoin", "btc", "btcpayserver", "btcpay server"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { + if (["bitcoin", "btc"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { return; } showPaymentInstructions(); From c1808164f2a0f02b3acae00d621ae59430c65d60 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 10:39:42 +0200 Subject: [PATCH 60/63] Log Shopify registration better --- .../Services/Shopify/ShopifyOrderMarkerHostedService.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs index c9af62ac9..4f2ccca2c 100644 --- a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -13,7 +13,6 @@ using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Shopify.Models; using BTCPayServer.Services.Stores; using Microsoft.Extensions.Logging; -using NBitpayClient; namespace BTCPayServer.Services.Shopify { @@ -57,7 +56,7 @@ namespace BTCPayServer.Services.Shopify await RegisterTransaction(invoice, shopifyOrderId, false); } - else if (new[] {Client.Models.InvoiceStatus.Complete, InvoiceStatus.Confirmed}.Contains( + else if (new[] {InvoiceStatus.Complete, InvoiceStatus.Confirmed}.Contains( invoice.Status)) { await RegisterTransaction(invoice, shopifyOrderId, true); @@ -93,8 +92,8 @@ namespace BTCPayServer.Services.Shopify invoice.Price.ToString(CultureInfo.InvariantCulture), success); if (resp != null) { - Logs.PayServer.LogInformation("Registered order transaction on Shopify. " + - $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); + Logs.PayServer.LogInformation($"Registered order transaction {invoice.Price}{invoice.Currency} on Shopify. " + + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}, Success: {success}"); } } catch (Exception ex) From d59295688efb45785466249568f040691489f906 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 28 Sep 2020 10:40:29 +0200 Subject: [PATCH 61/63] Handle edge cases around partial payments and when txs are not registered on Shopify because of unexpected outage --- .../StoresController.Integrations.cs | 93 ++++++++++++------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index c788c5b8e..6230d549b 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -10,6 +10,7 @@ using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Shopify; +using BTCPayServer.Services.Shopify.ApiModels; using BTCPayServer.Services.Shopify.Models; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; @@ -22,7 +23,6 @@ namespace BTCPayServer.Controllers { public partial class StoresController { - private static string _cachedShopifyJavascript; private async Task GetJavascript() @@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers } string[] fileList = _BtcpayServerOptions.BundleJsCss - ? new[] { "bundles/shopify-bundle.min.js"} + ? new[] {"bundles/shopify-bundle.min.js"} : new[] {"modal/btcpay.js", "shopify/btcpay-shopify.js"}; @@ -52,7 +52,8 @@ namespace BTCPayServer.Controllers [HttpGet("{storeId}/integrations/shopify/shopify.js")] public async Task ShopifyJavascript(string storeId) { - var jsFile = $"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\"; { await GetJavascript()}"; + var jsFile = + $"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\"; {await GetJavascript()}"; return Content(jsFile, "text/javascript"); } @@ -61,10 +62,10 @@ namespace BTCPayServer.Controllers [EnableCors(CorsPolicies.All)] [HttpGet("{storeId}/integrations/shopify/{orderId}")] public async Task ShopifyInvoiceEndpoint( - [FromServices] InvoiceRepository invoiceRepository, - [FromServices] InvoiceController invoiceController, + [FromServices] InvoiceRepository invoiceRepository, + [FromServices] InvoiceController invoiceController, [FromServices] IHttpClientFactory httpClientFactory, - string storeId, string orderId, bool checkOnly = false) + string storeId, string orderId, decimal amount, bool checkOnly = false) { var invoiceOrderId = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}"; var matchedExistingInvoices = await invoiceRepository.GetInvoices(new InvoiceQuery() @@ -75,7 +76,7 @@ namespace BTCPayServer.Controllers entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX) .Any(s => s == orderId)) .ToArray(); - + var firstInvoiceStillPending = matchedExistingInvoices.FirstOrDefault(entity => entity.GetInvoiceState().Status == InvoiceStatus.New); if (firstInvoiceStillPending != null) @@ -86,48 +87,78 @@ namespace BTCPayServer.Controllers status = firstInvoiceStillPending.Status.ToString().ToLowerInvariant() }); } - - var firstInvoiceSettled = - matchedExistingInvoices.FirstOrDefault(entity => new []{InvoiceStatus.Paid, InvoiceStatus.Complete, InvoiceStatus.Confirmed }.Contains(entity.GetInvoiceState().Status) ); - + + var firstInvoiceSettled = + matchedExistingInvoices.LastOrDefault(entity => + new[] {InvoiceStatus.Paid, InvoiceStatus.Complete, InvoiceStatus.Confirmed}.Contains( + entity.GetInvoiceState().Status)); + + var store = await _Repo.FindStore(storeId); + var shopify = store?.GetStoreBlob()?.Shopify; + ShopifyApiClient client = null; + ShopifyOrder order = null; + if (shopify?.IntegratedAt.HasValue is true) + { + client = new ShopifyApiClient(httpClientFactory, shopify.CreateShopifyApiCredentials()); + order = await client.GetOrder(orderId); + if (string.IsNullOrEmpty(order?.Id)) + { + return NotFound(); + } + } + if (firstInvoiceSettled != null) { - return Ok(new + //if BTCPay was shut down before the tx managed to get registered on shopify, this will fix it on the next UI load in shopify + if (client != null && order?.FinancialStatus == "pending" && + firstInvoiceSettled.Status != InvoiceStatus.Paid) { - invoiceId = firstInvoiceSettled.Id, - status = firstInvoiceSettled.Status.ToString().ToLowerInvariant() - }); + await new OrderTransactionRegisterLogic(client).Process(orderId, firstInvoiceSettled.Id, + firstInvoiceSettled.Currency, + firstInvoiceSettled.Price.ToString(CultureInfo.InvariantCulture), true); + order = await client.GetOrder(orderId); + } + + if (order?.FinancialStatus != "pending" && order?.FinancialStatus != "partially_paid") + { + return Ok(new + { + invoiceId = firstInvoiceSettled.Id, + status = firstInvoiceSettled.Status.ToString().ToLowerInvariant() + }); + } } if (checkOnly) { return Ok(); } - var store = await _Repo.FindStore(storeId); - var shopify = store?.GetStoreBlob()?.Shopify; + if (shopify?.IntegratedAt.HasValue is true) { - var client = new ShopifyApiClient(httpClientFactory, shopify.CreateShopifyApiCredentials()); - var order = await client.GetOrder(orderId); - if (string.IsNullOrEmpty(order?.Id) || order.FinancialStatus != "pending") + if (string.IsNullOrEmpty(order?.Id) || + !new[] {"pending", "partially_paid"}.Contains(order.FinancialStatus)) { return NotFound(); } + //we create the invoice at due amount provided from order page or full amount if due amount is bigger than order amount var invoice = await invoiceController.CreateInvoiceCoreRaw( - new CreateInvoiceRequest() {Amount = order.TotalPrice, Currency = order.Currency, Metadata = new JObject {["orderId"] = invoiceOrderId} }, store, + new CreateInvoiceRequest() + { + Amount = amount < order.TotalPrice ? amount : order.TotalPrice, + Currency = order.Currency, + Metadata = new JObject {["orderId"] = invoiceOrderId} + }, store, Request.GetAbsoluteUri(""), new List() {invoiceOrderId}); - return Ok(new - { - invoiceId = invoice.Id, - status = invoice.Status.ToString().ToLowerInvariant() - }); + return Ok(new {invoiceId = invoice.Id, status = invoice.Status.ToString().ToLowerInvariant()}); } + return NotFound(); } - + [HttpGet] [Route("{storeId}/integrations")] [Route("{storeId}/integrations/shopify")] @@ -156,10 +187,10 @@ namespace BTCPayServer.Controllers { ApiKey = userInfo[0], Password = userInfo[1], - ShopName = parsedUrl.Host.Replace(".myshopify.com", "", StringComparison.InvariantCultureIgnoreCase) + ShopName = parsedUrl.Host.Replace(".myshopify.com", "", + StringComparison.InvariantCultureIgnoreCase) }; command = "ShopifySaveCredentials"; - } catch (Exception) { @@ -167,6 +198,7 @@ namespace BTCPayServer.Controllers return View("Integrations", vm); } } + switch (command) { case "ShopifySaveCredentials": @@ -192,7 +224,6 @@ namespace BTCPayServer.Controllers } var scopesGranted = await apiClient.CheckScopes(); - //TODO: check if these are actually needed if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_orders")) { TempData[WellKnownTempData.ErrorMessage] = @@ -202,7 +233,7 @@ namespace BTCPayServer.Controllers // everything ready, proceed with saving Shopify integration credentials shopify.IntegratedAt = DateTimeOffset.Now; - + var blob = CurrentStore.GetStoreBlob(); blob.Shopify = shopify; if (CurrentStore.SetStoreBlob(blob)) From f1c1846c66780b0c25ac4ea8b8d7a79bd0d79646 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Tue, 29 Sep 2020 16:54:09 -0500 Subject: [PATCH 62/63] Restoring BtcPay keywords --- BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs | 2 +- BTCPayServer/wwwroot/shopify/btcpay-shopify.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index a43fc730d..6bb245669 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Services.Shopify private static TransactionDataHolder GetParentTransaction(List txs) { - var keywords = new[] {"bitcoin", "btc"}; + var keywords = new[] {"bitcoin", "btc", "btcpayserver", "btcpay server"}; return txs.FirstOrDefault(holder =>keywords .Any(s => holder.gateway.Contains(s, StringComparison.InvariantCultureIgnoreCase))); } diff --git a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js index 1a2ff14ef..2ba40d764 100644 --- a/BTCPayServer/wwwroot/shopify/btcpay-shopify.js +++ b/BTCPayServer/wwwroot/shopify/btcpay-shopify.js @@ -155,7 +155,7 @@ window.BTCPayShopifyIntegrationModule = function () { insertElement(buttonElement, pageItems.orderConfirmed); } - if (["bitcoin", "btc"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { + if (["bitcoin", "btc", "btcpayserver", "btcpay server"].filter(value => pageItems.paymentMethod.innerText.toLowerCase().indexOf(value) !== -1).length === 0) { return; } showPaymentInstructions(); From 0d4ca4520b6f84deec36f01885eb8577dd808a1c Mon Sep 17 00:00:00 2001 From: rockstardev Date: Tue, 29 Sep 2020 17:23:42 -0500 Subject: [PATCH 63/63] Ensuring matching of keywords only on first transaction --- .../Shopify/OrderTransactionRegisterLogic.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs index 6bb245669..1465bf071 100644 --- a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -16,22 +16,18 @@ namespace BTCPayServer.Services.Shopify _client = client; } - private static TransactionDataHolder GetParentTransaction(List txs) - { - var keywords = new[] {"bitcoin", "btc", "btcpayserver", "btcpay server"}; - return txs.FirstOrDefault(holder =>keywords .Any(s => holder.gateway.Contains(s, StringComparison.InvariantCultureIgnoreCase))); - } - + private static string[] _keywords = new[] { "bitcoin", "btc", "btcpayserver", "btcpay server" }; public async Task Process(string orderId, string invoiceId, string currency, string amountCaptured, bool success) { currency = currency.ToUpperInvariant().Trim(); var existingShopifyOrderTransactions = (await _client.TransactionsList(orderId)).transactions; - var baseParentTransaction = GetParentTransaction(existingShopifyOrderTransactions); - if (baseParentTransaction is null) + var baseParentTransaction = existingShopifyOrderTransactions.FirstOrDefault(); + if (baseParentTransaction is null || + !_keywords.Any(a => baseParentTransaction.gateway.Contains(a, StringComparison.InvariantCultureIgnoreCase))) { return null; } - + //technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint. if (currency.ToUpperInvariant().Trim() != baseParentTransaction.currency.ToUpperInvariant().Trim()) { @@ -48,17 +44,17 @@ namespace BTCPayServer.Services.Shopify //find all existing transactions recorded around this invoice id var existingShopifyOrderTransactionsOnSameInvoice = existingShopifyOrderTransactions.Where(holder => holder.authorization == invoiceId); - + //filter out the successful ones var successfulActions = existingShopifyOrderTransactionsOnSameInvoice.Where(holder => holder.status == "success").ToArray(); //of the successful ones, get the ones we registered as a valid payment var successfulCaptures = successfulActions.Where(holder => holder.kind == "capture").ToArray(); - + //of the successful ones, get the ones we registered as a voiding of a previous successful payment var refunds = successfulActions.Where(holder => holder.kind == "refund").ToArray(); - + //if we are working with a non-success registration, but see that we have previously registered this invoice as a success, we switch to creating a "void" transaction, which in shopify terms is a refund. if (!success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0) { @@ -67,7 +63,7 @@ namespace BTCPayServer.Services.Shopify status = "success"; } //if we are working with a success registration, but can see that we have already had a successful transaction saved, get outta here - else if(success && successfulCaptures.Length >0 && (successfulCaptures.Length - refunds.Length ) > 0 ) + else if (success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0) { return null; }