Compare commits

...

15 Commits

Author SHA1 Message Date
Pavlenex
8e833894ce
Update README.md
update image with a link to avoid doc.btcpayserver broken relative path preview
2024-01-23 13:26:51 +01:00
Pavlenex
b8e56e627c
Update README.md (#4)
* Update README.md
* add banner
2024-01-19 14:08:48 +01:00
Andrew Camilleri
9ac08ba705
Merge pull request #1 from ndeet/improvements
Updating READMEs; fixing permission and update docker-compose contain…
2023-11-14 18:11:06 +01:00
Kukks
6f367a1c94
update permission and bump 2023-11-09 14:02:33 +01:00
ndeet
86daaf419b Updating READMEs; fixing permission and update docker-compose container name. 2023-11-09 12:46:26 +01:00
Kukks
44f4aea5f6
Add btcpay rate provider, improve order notes, add btc as currency 2023-11-09 10:09:37 +01:00
Kukks
ca62b7ed73
fixes 2023-11-08 16:13:26 +01:00
Kukks
05c79fb545
make bash script determine its own loc 2023-11-08 13:52:44 +01:00
Kukks
ff26502a6a
ensaure license includes btcpay too 2023-11-08 11:33:08 +01:00
Kukks
bcb889c3f3
add info to readme 2023-11-08 11:32:42 +01:00
Kukks
b97a117096
make docker compose standalone 2023-11-08 11:26:45 +01:00
Kukks
b9a3d46db3
upd wf 2023-11-08 11:11:19 +01:00
Kukks
ff86808af4
auto build package with tag 2023-11-08 11:01:12 +01:00
Kukks
8d359fb4ce
create quick packager 2023-11-08 09:28:04 +01:00
Kukks
80be645609
update 2023-11-08 08:41:47 +01:00
25 changed files with 536 additions and 484 deletions

View File

@ -1,47 +0,0 @@
name: Delete old workflow runs
on:
schedule:
# Run monthly, at 00:00 on the 1st day of month.
- cron: '0 0 1 * *'
workflow_dispatch:
inputs:
days:
description: 'Number of days.'
required: true
default: '30'
minimum_runs:
description: 'The minimum runs to keep for each workflow.'
required: true
default: '6'
delete_workflow_pattern:
description: 'The name or filename of the workflow. if not set then it will target all workflows.'
required: false
delete_workflow_by_state_pattern:
description: 'Remove workflow by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually'
required: true
default: "All"
type: choice
options:
- "All"
- active
- deleted
- disabled_inactivity
- disabled_manually
dry_run:
description: 'Only log actions, do not perform any delete operations.'
required: false
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: ${{ github.event.inputs.days }}
keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}
delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }}
delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }}
dry_run: ${{ github.event.inputs.dry_run }}

58
.github/workflows/publish-btcpay.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Create Release
on:
create:
tags:
- 'BTCPayServer/*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up .NET 7
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.x' # Use .NET 7
- name: Run build script and capture output
run: |
chmod +x $GITHUB_WORKSPACE/build-btcpayplugin.sh
ASSET_PATH=$($GITHUB_WORKSPACE/build-btcpayplugin.sh | tail -1)
echo "ASSET_PATH=$ASSET_PATH" >> $GITHUB_ENV
- name: Verify tag matches asset file name
run: |
TAG_NAME=${GITHUB_REF#refs/tags/BTCPayServer/}
ASSET_BASE_NAME=$(basename $ASSET_PATH .zip)
if [[ "$ASSET_BASE_NAME" != *"$TAG_NAME" ]]; then
echo "Error: Tag name does not match asset file name."
exit 1
fi
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Extract file name
run: echo "ASSET_NAME=$(basename ${{ env.ASSET_PATH }})" >> $GITHUB_ENV
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.ASSET_PATH }}
asset_name: ${{ env.ASSET_NAME }}
asset_content_type: application/zip

View File

@ -1,75 +0,0 @@
name: Publish nightly dev Docker image
on:
# Manual trigger
workflow_dispatch:
# At 06:00 AM, on Tuesday, Thursday, and Saturday
schedule:
- cron: "0 6 * * TUE,THU,SAT"
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
publish-nightly:
name: Build, Test, Publish and Deploy nightly Docker image
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 7.0.x
include-prerelease: true
- name: Build
run: dotnet build Smartstore.sln -c Release
- name: Test
run: dotnet test Smartstore.sln -c Release --no-restore --no-build
- name: Log in to the GitHub Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
#- name: Log in to Docker Hub
# uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
# with:
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
- name: Publish and Push for Linux
if: matrix.os == 'ubuntu-latest'
run: |
dotnet publish src/Smartstore.Web/Smartstore.Web.csproj -c Release -o ./.build/release --no-restore --no-build --no-self-contained
docker build --build-arg SOURCE=./.build/release -f NoBuild.Dockerfile -t ghcr.io/smartstore/smartstore-linux:nightly .
docker push ghcr.io/smartstore/smartstore-linux:nightly
#docker build --build-arg VERSION=nightly -f NoBuild.Dockerfile -t smartstore/smartstore-linux:nightly .
#docker push smartstore/smartstore-linux:nightly
- name: Publish and Push for Windows
if: matrix.os == 'windows-latest'
run: |
dotnet publish src/Smartstore.Web/Smartstore.Web.csproj -c Release -o ./.build/release --no-restore --no-build --no-self-contained
docker build --build-arg SOURCE=./.build/release -f Nano.Dockerfile -t ghcr.io/smartstore/smartstore-windows:nightly .
docker push ghcr.io/smartstore/smartstore-windows:nightly
#docker build --build-arg VERSION=nightly -f NoBuild.Dockerfile -t smartstore/smartstore-windows:nightly .
#docker push smartstore/smartstore-windows:nightly

View File

@ -1,70 +0,0 @@
name: Publish release Docker image
on:
push:
branches:
- '5.0.5.x' # << Change this for new versions.
workflow_dispatch:
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
publish-release:
name: Build, Publish and Deploy stable release Docker image
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Format branch label
id: format-branch-label
run: bash .github/FormatBranchLabel.sh
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x # << Change this to current .NET version.
include-prerelease: false
- name: Build
run: dotnet build Smartstore.sln -c Release
- name: Log in to the GitHub Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish and Push for Linux
if: matrix.os == 'ubuntu-latest'
run: |
dotnet publish src/Smartstore.Web/Smartstore.Web.csproj -c Release -o ./.build/release --no-restore --no-build --no-self-contained
docker build --build-arg SOURCE=./.build/release -f NoBuild.Dockerfile -t ghcr.io/smartstore/smartstore-linux:latest -t ghcr.io/smartstore/smartstore-linux:${{ steps.format-branch-label.outputs.branch-label }} .
docker push ghcr.io/smartstore/smartstore-linux:latest
docker push ghcr.io/smartstore/smartstore-linux:${{ steps.format-branch-label.outputs.branch-label }}
- name: Publish and Push for Windows
if: matrix.os == 'windows-latest'
run: |
dotnet publish src/Smartstore.Web/Smartstore.Web.csproj -c Release -o ./.build/release --no-restore --no-build --no-self-contained
docker build --build-arg SOURCE=./.build/release -f Nano.Dockerfile -t ghcr.io/smartstore/smartstore-windows:latest -t ghcr.io/smartstore/smartstore-windows:${{ steps.format-branch-label.outputs.branch-label }} .
docker push ghcr.io/smartstore/smartstore-windows:latest
docker push ghcr.io/smartstore/smartstore-windows:${{ steps.format-branch-label.outputs.branch-label }}

View File

@ -1,3 +1,21 @@
**NOTE: This fork of Smartstore hosts the plugin source code for the BTCPay Server integration. You can view its README at [src/Smartstore.Modules/Smartstore.BTCPayServer/README.md](src/Smartstore.Modules/Smartstore.BTCPayServer/README.md).**
## Building the plugin
Running `build-btcpayplugin.ps1` or `build-btcpayplugin.sh` will build the plugin and place the resulting zip file in `build/packages` which you can then use to install.
## Releasing the plugin
A github actions workflow is configured to automatically build and release the plugin when a new tag is pushed to the repository.
The workflow will build the plugin and upload it to the [releases page](https://github.com/btcpayserver/Smartstore.BTCPayServer/releases/)
The tag name must be in the format `BTCPayServer/{version}`, where `{version}` is the version of the plugin as defined in `src\Smartstore.Modules\Smartstore.BTCPayServer\module.json`.
## Testing without debugging
Simply run `docker compose up` in this directory and the latest version of SmartStore will be run on `http://localhost`. Just use the SQLite db option and you're good to go.
---
---
<p align="center">
<a href="https://www.smartstore.com" target="_blank" rel="noopener noreferrer">
<img src="assets/smartstore-icon-whitebg.png" alt="Smartstore" width="120">

View File

@ -83,6 +83,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Smartstore.Data.PostgreSql"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Smartstore.Data.Sqlite", "src\Smartstore.Data\Smartstore.Data.Sqlite\Smartstore.Data.Sqlite.csproj", "{32683DB7-C20D-4ABD-BE90-569168B2A68F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Smartstore.BTCPayServer", "src\Smartstore.Modules\Smartstore.BTCPayServer\Smartstore.BTCPayServer.csproj", "{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Smartstore.Packager", "tools\Smartstore.Packager\Smartstore.Packager.csproj", "{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Smartstore.PackagerCLI", "tools\Smartstore.PackagerCLI\Smartstore.PackagerCLI.csproj", "{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -270,6 +276,24 @@ Global
{32683DB7-C20D-4ABD-BE90-569168B2A68F}.DebugNoRazorCompile|Any CPU.Build.0 = DebugNoRazorCompile|Any CPU
{32683DB7-C20D-4ABD-BE90-569168B2A68F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32683DB7-C20D-4ABD-BE90-569168B2A68F}.Release|Any CPU.Build.0 = Release|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.DebugNoRazorCompile|Any CPU.ActiveCfg = DebugNoRazorCompile|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.DebugNoRazorCompile|Any CPU.Build.0 = DebugNoRazorCompile|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3}.Release|Any CPU.Build.0 = Release|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.DebugNoRazorCompile|Any CPU.ActiveCfg = Debug|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.DebugNoRazorCompile|Any CPU.Build.0 = Debug|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B59EAEF6-5E27-42C9-9BF3-0201BE82147B}.Release|Any CPU.Build.0 = Release|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.DebugNoRazorCompile|Any CPU.ActiveCfg = Debug|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.DebugNoRazorCompile|Any CPU.Build.0 = Debug|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AB115E0-CB09-4A2B-B3D5-3BB42A321957}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -301,6 +325,7 @@ Global
{0C26B088-A8F6-42E8-AB8D-D9D297FDD72C} = {E5C1AA43-6B44-4EFA-8DE9-6070EE007CE3}
{1A016177-D0EA-49AD-9BD5-BAD99D9D08F9} = {82A38DDA-C944-4C15-AF43-86732F8ED15A}
{32683DB7-C20D-4ABD-BE90-569168B2A68F} = {82A38DDA-C944-4C15-AF43-86732F8ED15A}
{5580D90E-3E5F-4D4A-8DFC-DE7969FBB9C3} = {E5C1AA43-6B44-4EFA-8DE9-6070EE007CE3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A0A0B995-5D0B-458E-BCC0-1071B76949D1}

5
build-btcpayplugin.ps1 Normal file
View File

@ -0,0 +1,5 @@
$StartPath = Split-Path -Parent $PSCommandPath
echo "$StartPath"
dotnet build $StartPath/src/Smartstore.Modules/Smartstore.BTCPayServer/Smartstore.BTCPayServer.csproj -c Release
echo "$StartPath/tools/Smartstore.PackagerCLI/Smartstore.PackagerCLI.csproj"
dotnet run --project $StartPath/tools/Smartstore.PackagerCLI/Smartstore.PackagerCLI.csproj "$StartPath/src/Smartstore.Web/Modules/Smartstore.BTCPayServer" "$StartPath/build/packages"

16
build-btcpayplugin.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# Get the absolute directory path of the currently running script
StartPath=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
# Print the StartPath
echo "$StartPath"
# Build the project
dotnet build "$StartPath/src/Smartstore.Modules/Smartstore.BTCPayServer/Smartstore.BTCPayServer.csproj" -c Release
# Print the path to the PackagerCLI project
echo "$StartPath/tools/Smartstore.PackagerCLI/Smartstore.PackagerCLI.csproj"
# Run the PackagerCLI project
dotnet run --project "$StartPath/tools/Smartstore.PackagerCLI/Smartstore.PackagerCLI.csproj" "$StartPath/src/Smartstore.Web/Modules/Smartstore.BTCPayServer" "$StartPath/build/packages"

View File

@ -2,28 +2,7 @@ version: "3.4"
services:
web:
image: ghcr.io/smartstore/smartstore-linux
container_name: web
container_name: smartstore_btcpay
ports:
- "80:80"
restart: unless-stopped
depends_on:
- db
volumes:
- "D:/mount/smtenants/mysql:/app/App_Data/Tenants"
db:
image: mysql
container_name: mysql
environment:
#MYSQL_DATABASE: smartstore
#MYSQL_USER: "root"
MYSQL_PASSWORD: "Smartstore2022!"
MYSQL_ROOT_PASSWORD: "Smartstore2022!"
ports:
- '3307:3306'
expose:
- '3306'
volumes:
- mysql:/var/lib/mysql
volumes:
mysql:

View File

@ -10,6 +10,7 @@ using Smartstore.Core;
using System.Web;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Smartstore.BTCPayServer.Configuration;
using Smartstore.BTCPayServer.Services;
@ -18,26 +19,26 @@ using Smartstore.Core.Checkout.Payment;
namespace Smartstore.BTCPayServer.Controllers
{
[Area("Admin")]
[Route("[area]/btcpay/{action=index}/{id?}")]
[Route("[area]/btcpayserver/{action=index}/{id?}")]
public class BtcPayAdminController : ModuleController
{
private readonly ICommonServices _services;
private readonly BtcPayService _btcPayService;
private readonly LinkGenerator _linkGenerator;
private readonly IProviderManager _providerManager;
private readonly ICurrencyService _currencyService;
private readonly PaymentSettings _paymentSettings;
public BtcPayAdminController(
BtcPayService btcPayService,
ILogger<BtcPayAdminController> logger,
ICommonServices services,
IProviderManager providerManager,
ICurrencyService currencyService,
LinkGenerator linkGenerator,
ICommonServices services,
IProviderManager providerManager,
ICurrencyService currencyService,
PaymentSettings settings)
{
_btcPayService = btcPayService;
_linkGenerator = linkGenerator;
_providerManager = providerManager;
_currencyService = currencyService;
_paymentSettings = settings;
@ -49,11 +50,15 @@ namespace Smartstore.BTCPayServer.Controllers
{
var model = MiniMapper.Map<BtcPaySettings, ConfigurationModel>(settings);
var myStore = _services.StoreContext.CurrentStore;
ViewBag.Provider = _providerManager.GetProvider("Smartstore.BTCPayServer").Metadata;
ViewBag.StoreCurrencyCode = _currencyService.PrimaryCurrency.CurrencyCode ?? "EUR";
ViewBag.UrlWebHook = myStore.Url + "BtcPayHook/Process";
ViewBag.UrlWebHook = new Uri(new Uri(myStore.Url),
_linkGenerator.GetPathByAction("Process", "BtcPayHook"));
var sViewMsg = HttpContext.Session.GetString("ViewMsg");
if (!string.IsNullOrEmpty(sViewMsg))
{
@ -68,16 +73,6 @@ namespace Smartstore.BTCPayServer.Controllers
HttpContext.Session.SetString("ViewMsgError", "");
}
var sUrl = "";
if (!string.IsNullOrEmpty(model.BtcPayUrl))
{
sUrl = model.BtcPayUrl + (model.BtcPayUrl.EndsWith("/") ? "" : "/");
sUrl += $"api-keys/authorize?applicationName={myStore.Name.Replace(" ", "")}&applicationIdentifier=SmartStore{myStore.Id}&selectiveStores=true"
+ $"&redirect={myStore.Url}admin/btcpay/getautomaticapikeyconfig&permissions=btcpay.store.canmodifystoresettings";
}
ViewBag.UrlBtcApiKey = sUrl;
ViewBag.UrlCreateWebHook = myStore.Url + "admin/btcpay/createwebhook/";
return View(model);
}
@ -90,19 +85,26 @@ namespace Smartstore.BTCPayServer.Controllers
}
var myStore = _services.StoreContext.CurrentStore;
var adminUrl = myStore.Url + "admin/btcpay/getautomaticapikeyconfig";
var adminUrlParams = new Dictionary<string, string>();
adminUrlParams.Add("ssid", myStore.Id.ToString());
adminUrlParams.Add("btcpayuri", btcpayUri.ToString());
adminUrl += QueryString.Create(adminUrlParams);
var uri = BTCPayServerClient.GenerateAuthorizeUri(btcpayUri, new[] {"btcpay.store.canmodifystoresettings"},
true, true, ($"SmartStore{myStore.Id}", new Uri(adminUrl)));
var adminUrl = new Uri(new Uri(myStore.Url),
_linkGenerator.GetPathByAction(HttpContext,"GetAutomaticApiKeyConfig", "BtcPayAdmin",
new {ssid = myStore.Id, btcpayuri = btcpayUri}));
var uri = BTCPayServerClient.GenerateAuthorizeUri(btcpayUri,
new[]
{
Policies.CanCreateInvoice, // create invoices for payment
Policies.CanViewInvoices, // fetch created invoices to check status
Policies.CanModifyInvoices, // able to mark an invoice invalid in case merchant wants to void the order
Policies.CanModifyStoreWebhooks, // able to create the webhook required automatically
Policies.CanViewStoreSettings, // able to fetch rates
Policies.CanCreateNonApprovedPullPayments // able to create refunds
},
true, true, ($"SmartStore{myStore.Id}", adminUrl));
return uri + $"&applicationName={HttpUtility.UrlEncode(myStore.Name)}";
}
[HttpPost, SaveSetting, AuthorizeAdmin]
public async Task<IActionResult> Configure(ConfigurationModel model, BtcPaySettings settings , string command= null)
public async Task<IActionResult> Configure(ConfigurationModel model, BtcPaySettings settings,
string command = null)
{
if (command == "delete")
{
@ -112,7 +114,7 @@ namespace Smartstore.BTCPayServer.Controllers
settings.BtcPayUrl = "";
ModelState.Clear();
_paymentSettings.ActivePaymentMethodSystemNames.Remove("Smartstore.BTCPayServer");
HttpContext.Session.SetString("ViewMsg", "Settings cleared and payment method deactivated");
return Configure(settings);
}
@ -120,15 +122,14 @@ namespace Smartstore.BTCPayServer.Controllers
if (command == "activate" && model.IsConfigured())
{
_paymentSettings.ActivePaymentMethodSystemNames.Add("Smartstore.BTCPayServer");
await _services.SettingFactory.SaveSettingsAsync(_paymentSettings);
HttpContext.Session.SetString("ViewMsg", "Payment method activated");
}
if (command == "getautomaticapikeyconfig")
{
MiniMapper.Map(model, settings);
string? result = GetRedirectUri(settings);
if (result != null)
@ -160,75 +161,59 @@ namespace Smartstore.BTCPayServer.Controllers
public async Task<IActionResult> GetAutomaticApiKeyConfig()
{
Request.Query.TryGetValue("ssid", out var ssidx);
var ssid = int.Parse(ssidx.FirstOrDefault() ?? _services.StoreContext.CurrentStore.Id.ToString());
var settings = await _services.SettingFactory.LoadSettingsAsync<BtcPaySettings>(ssid);
try
var ssid = int.Parse(ssidx.FirstOrDefault() ?? _services.StoreContext.CurrentStore.Id.ToString());
if (ssid != _services.StoreContext.CurrentStore.Id)
{
return NotFound();
}
var settings = await _services.SettingFactory.LoadSettingsAsync<BtcPaySettings>(ssid);
try
{
Request.Form.TryGetValue("apiKey", out var apiKey);
Request.Form.TryGetValue("permissions[]", out var permissions);
Permission.TryParse(permissions.FirstOrDefault(), out var permission);
if (Request.Query.TryGetValue("btcpayuri", out var btcpayUris) && btcpayUris.FirstOrDefault() is { } stringbtcpayUri)
if (Request.Query.TryGetValue("btcpayuri", out var btcpayUris) &&
btcpayUris.FirstOrDefault() is { } stringbtcpayUri)
{
settings.BtcPayUrl = stringbtcpayUri;
}
settings.ApiKey = apiKey;
settings.BtcPayStoreID = permission.Scope;
try
{
if (permission.Scope is null)
{
settings.BtcPayStoreID = await _btcPayService.GetStoreId(settings);
settings.BtcPayStoreID = await _btcPayService.GetStoreId(settings);
}
if(string.IsNullOrEmpty(settings.WebHookSecret))
if (string.IsNullOrEmpty(settings.WebHookSecret))
{
settings.WebHookSecret = await _btcPayService.CreateWebHook(settings, _services.StoreContext.CurrentStore.Url + "BtcPayHook/Process");
var webhookUrl = new Uri(new Uri(_services.StoreContext.CurrentStore.Url),
_linkGenerator.GetPathByAction("Process", "BtcPayHook"));
settings.WebHookSecret = await _btcPayService.CreateWebHook(settings, webhookUrl.ToString());
}
}
catch (Exception e)
{
}
_paymentSettings.ActivePaymentMethodSystemNames.Add("Smartstore.BTCPayServer");
await _services.SettingFactory.SaveSettingsAsync(_paymentSettings);
await _services.SettingFactory.SaveSettingsAsync(_paymentSettings);
HttpContext.Session.SetString("ViewMsg", "API Key and Store ID set with success. Don't forget to click on <b>Save</b> button to update data !");
await _services.SettingFactory.SaveSettingsAsync(settings);
HttpContext.Session.SetString("ViewMsg",
"Settings automatically configured and payment method activated.");
}
catch (Exception ex)
{
HttpContext.Session.SetString("ViewMsgError", "Error during API Key creation !");
HttpContext.Session.SetString("ViewMsgError", "Error during automatic configuration");
Logger.Error(ex.Message);
}
return RedirectToAction(nameof(Configure), settings);
return RedirectToAction(nameof(Configure));
}
[HttpGet]
public async Task<IActionResult> CreateWebHook()
{
var myStore = _services.StoreContext.CurrentStore;
var settings = await _services.SettingFactory.LoadSettingsAsync<BtcPaySettings>(myStore.Id);
if (! (string.IsNullOrEmpty(settings.BtcPayStoreID)
|| string.IsNullOrEmpty(settings.BtcPayUrl)
|| string.IsNullOrEmpty(settings.ApiKey)))
{
try
{
settings.WebHookSecret = await _btcPayService.CreateWebHook(settings, myStore.Url + "BtcPayHook/Process");
HttpContext.Session.SetString("ViewMsg", "WebHook created successfully. Don't forget to click on <b>Save</b> button to update data !");
}
catch (Exception ex)
{
HttpContext.Session.SetString("ViewMsgError", "Error during WebHook creation! Make sure your API Key and Store ID are correct.");
Logger.Error(ex.Message);
}
}
return RedirectToAction(nameof(Configure), settings);
}
}
}

View File

@ -1,5 +1,5 @@
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -9,35 +9,31 @@ using Smartstore.BTCPayServer.Configuration;
using Smartstore.BTCPayServer.Providers;
using Smartstore.BTCPayServer.Services;
using Smartstore.Core;
using Smartstore.Core.Checkout.Orders;
using Smartstore.Core.Checkout.Payment;
using Smartstore.Core.Data;
using Smartstore.Web.Controllers;
namespace Smartstore.BTCPayServer.Controllers
{
public class BtcPayHookController : PublicController
{
private readonly ILogger _logger;
private readonly BtcPayService _btcPayService;
private readonly SmartDbContext _db;
private readonly BtcPaySettings _settings;
private readonly ICommonServices _services;
public BtcPayHookController(
BtcPaySettings settings,
SmartDbContext db,
ILogger logger,
ICommonServices services,
BtcPayService btcPayService)
{
_logger = logger;
_btcPayService = btcPayService;
_db = db;
_settings = settings;
_services = services;
}
[HttpPost]
[HttpPost][AllowAnonymous]
public async Task<IActionResult> Process([FromHeader(Name = "BTCPAY-SIG")] string btcPaySig)
{
try
@ -45,7 +41,7 @@ namespace Smartstore.BTCPayServer.Controllers
string jsonStr = await new StreamReader(Request.Body).ReadToEndAsync();
var webhookEvent = JsonConvert.DeserializeObject<WebhookInvoiceEvent>(jsonStr);
var signature = btcPaySig.Split('=')[1];
if(webhookEvent?.InvoiceId?.StartsWith("__test__") is true)
if(webhookEvent is null || webhookEvent?.InvoiceId?.StartsWith("__test__") is true || webhookEvent?.Type == WebhookEventType.InvoiceCreated)
{
return Ok();
}
@ -55,26 +51,24 @@ namespace Smartstore.BTCPayServer.Controllers
Logger.Error("Missing fields in request");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
if (_settings.WebHookSecret is not null && !BtcPayService.CheckSecretKey(_settings.WebHookSecret, jsonStr, signature))
{
Logger.Error("Bad secret key");
return StatusCode(StatusCodes.Status400BadRequest);
}
var invoice = await _btcPayService.GetInvoice(_settings, webhookEvent.InvoiceId);
if( invoice is null)
{
Logger.Error("Invoice not found");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
var order = await _db.Orders.FirstOrDefaultAsync(x =>
x.PaymentMethodSystemName == BTCPayPaymentProvider.SystemName &&
x.OrderGuid == new Guid(orderId));
if (order == null)
if (order is null)
{
Logger.Error("Order not found");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
var settings = await _services.SettingFactory.LoadSettingsAsync<BtcPaySettings>(order.StoreId);
if (settings.WebHookSecret is not null && !BtcPayService.CheckSecretKey(settings.WebHookSecret, jsonStr, signature))
{
Logger.Error("Bad secret key");
return StatusCode(StatusCodes.Status400BadRequest);
}
var invoice = await _btcPayService.GetInvoice(settings, webhookEvent.InvoiceId);
if (await _btcPayService.UpdateOrderWithInvoice(order, invoice, webhookEvent))
{

View File

@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Smartstore.Core.Data;
using Smartstore.Web.Controllers;
namespace Smartstore.BTCPayServer.Controllers
{
[Route("btcpayserver/order")]
public class BtcPayOrderController : PublicController
{
private readonly SmartDbContext _db;
public BtcPayOrderController(
SmartDbContext db)
{
_db = db;
}
[HttpGet("{id}")]
public async Task<IActionResult> Index(Guid id)
{
var orderId = (await _db.Orders.SingleOrDefaultAsync(order => order.OrderGuid == id))?.Id;
if (orderId is null)
{
return NotFound();
}
return RedirectToAction("Details", "Order", new {id = orderId});
}
}
}

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 Nisaba
Copyright (c) 2023 BTCPay Server
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,34 +1,32 @@
<Language Name="Deutsch" IsDefault="false" IsRightToLeft="false">
<LocaleResource Name="Plugins.FriendlyName.SmartStore.BTCPayServer" AppendRootKey="false">
<Value>Bezahlen Sie mit Bitcoin</Value>
</LocaleResource>
<LocaleResource Name="Plugins.Description.SmartStore.BTCPayServer" AppendRootKey="false">
<Value>Akzeptieren Sie Bitcoin in Ihrem SmartStore-Shop mit BTCPay Server</Value>
</LocaleResource>
<LocaleResource Name="Plugins.FriendlyName.SmartStore.BTCPayServer" AppendRootKey="false">
<Value>Bezahlen Sie mit Bitcoin</Value>
</LocaleResource>
<LocaleResource Name="Plugins.Description.SmartStore.BTCPayServer" AppendRootKey="false">
<Value>Akzeptieren Sie Bitcoin in Ihrem SmartStore-Shop mit BTCPay Server</Value>
</LocaleResource>
<!-- (Provider name) will be displayed in frontend on payment selection page and in backend on payment configuration list -->
<LocaleResource Name="Plugins.FriendlyName.Payments.BTCPayServer" AppendRootKey="false">
<Value>BTCPay Server</Value>
</LocaleResource>
<!-- (Provider description) will be displayed in frontend on payment selection page and in backend on payment configuration list -->
<LocaleResource Name="Plugins.Description.Payments.BTCPayServer" AppendRootKey="false">
<Value>Akzeptieren Sie Bitcoin in Ihrem SmartStore-Shop mit BtcPay Server.</Value>
</LocaleResource>
<!-- (Provider name) will be displayed in frontend on payment selection page and in backend on payment configuration list -->
<LocaleResource Name="Plugins.FriendlyName.Payments.BTCPayServer" AppendRootKey="false">
<Value>BTCPay Server</Value>
</LocaleResource>
<!-- (Provider description) will be displayed in frontend on payment selection page and in backend on payment configuration list -->
<LocaleResource Name="Plugins.Description.Payments.BTCPayServer" AppendRootKey="false">
<Value>Akzeptieren Sie Bitcoin in Ihrem SmartStore-Shop mit BtcPay Server.</Value>
</LocaleResource>
<LocaleResource Name="Plugins.SmartStore.BTCPayServer" AppendRootKey="false">
<Children>
<LocaleResource Name="AdminInstruction">
<Value>
<![CDATA[
<LocaleResource Name="Plugins.SmartStore.BTCPayServer" AppendRootKey="false">
<Children>
<LocaleResource Name="AdminInstruction">
<Value>
<![CDATA[
<div class="mb-1"><a href="https://github.com/Nisaba/btcpay-smartstore-plugin" target="_blank">BTCPay Plugin für SmartStore</a></div>
<div class="mb-1">Die Konfiguration des Plugins kann automatisch oder manuell erfolgen.</div>
<div class="mb-1"><br/><b>Automatische Konfiguration:</b></div>
<ul>
<li>Geben Sie den "BTCPay Url"-Parameter ein und speichern Sie ihn.</li>
<li>Klicken Sie auf den Link "API-Key automatisch erstellen", um zur Seite zur Erstellung des Schlüssels auf Ihrem BTCPay-Server weitergeleitet zu werden.</li>
<li>Die Parameter "API-Key" und "BTCPay Store ID" werden automatisch ausgefüllt. Speichern Sie sie.</li>
<li>Klicken Sie auf den Link "WebHook automatisch erstellen".</li>
<li>Das Feld "WebHook Secret" wird automatisch ausgefüllt. Speichern Sie es.</li>
<li>Geben Sie den "BTCPay Url"-Parameter ein und speichern .</li>
<li>Klicken Sie auf "Configure automatically", um zur Seite zur Erstellung des Schlüssels auf Ihrem BTCPay-Server weitergeleitet zu werden.</li>
<li>Die Parameter "API-Key", "BTCPay Store ID" und "WebHook Secret" werden automatisch ausgefüllt. Speichern Sie sie.</li>
</ul>
<div class="mb-1"><br/><b>Manuelle Konfiguration:</b></div>
<ul>
@ -41,53 +39,53 @@
<li>Um den BTCPay WebHook zu erstellen, <a href="https://docs.btcpayserver.org/VirtueMart/#23-create-a-webhook-on-btcpay-server" target="_blank">lesen Sie dies</a>. (verwenden Sie den standardmäßig von BTCPay generierten Geheimcode)</li>
</ul>
]]>
</Value>
</LocaleResource>
</Value>
</LocaleResource>
<LocaleResource Name="WebHookNote">
<Value>
<![CDATA[
<LocaleResource Name="WebHookNote">
<Value>
<![CDATA[
Hinweis: Beim Testen des Webhooks von BTCPay sollte ein HTTP 422-Fehler angezeigt werden.<br/>
Dies liegt daran, dass BTCPay leere Daten sendet, während das SmartStore-Plugin echte Daten erwartet.<br/>
Dieser Fehler weist daher darauf hin, dass der Webhook tatsächlich über BTCPay zugänglich ist.<br/>
Bei einer echten Transaktion können wir daher von einer korrekten Abwicklung ausgehen.
]]>
</Value>
</LocaleResource>
</Value>
</LocaleResource>
<LocaleResource Name="WebHookInfo">
<LocaleResource Name="WebHookInfo">
<Value>Hier ist die URL, die für die WebHook-Erstellung in BTCPay festgelegt werden muss: </Value>
</LocaleResource>
<LocaleResource Name="BtcPayUrl">
<Value>BTCPay-URL</Value>
</LocaleResource>
<LocaleResource Name="BtcPayUrl.Hint">
<Value>Die URL Ihrer BTCPay-Instanz</Value>
</LocaleResource>
<LocaleResource Name="ApiKey">
<Value>API-Schlüssel</Value>
</LocaleResource>
<LocaleResource Name="ApiKey.Hint">
<Value>Der in Ihrer BTCPay-Instanz generierte API-Schlüsselwert</Value>
</LocaleResource>
<Value>BTCPay-URL</Value>
</LocaleResource>
<LocaleResource Name="BtcPayUrl.Hint">
<Value>Die URL Ihrer BTCPay-Instanz</Value>
</LocaleResource>
<LocaleResource Name="ApiKey">
<Value>API-Schlüssel</Value>
</LocaleResource>
<LocaleResource Name="ApiKey.Hint">
<Value>Der in Ihrer BTCPay-Instanz generierte API-Schlüsselwert</Value>
</LocaleResource>
<LocaleResource Name="CreateApiKey">
<Value>API-Schlüssel automatisch erstellen</Value>
</LocaleResource>
<LocaleResource Name="BtcPayStoreID">
<Value>BTCPay Store-ID</Value>
</LocaleResource>
<LocaleResource Name="BtcPayStoreID.Hint">
<Value>die BTCPay Store ID</Value>
</LocaleResource>
<LocaleResource Name="WebHookSecret">
<Value>WebHook-Geheimnis</Value>
</LocaleResource>
<LocaleResource Name="WebHookSecret.Hint">
<Value>Der in Ihrer BTCPay-Instanz generierte WebHook-Secret-Wert</Value>
</LocaleResource>
<Value>BTCPay Store-ID</Value>
</LocaleResource>
<LocaleResource Name="BtcPayStoreID.Hint">
<Value>die BTCPay Store ID</Value>
</LocaleResource>
<LocaleResource Name="WebHookSecret">
<Value>WebHook-Geheimnis</Value>
</LocaleResource>
<LocaleResource Name="WebHookSecret.Hint">
<Value>Der in Ihrer BTCPay-Instanz generierte WebHook-Secret-Wert</Value>
</LocaleResource>
<LocaleResource Name="CreateWebhook">
<Value>Webhook automatisch erstellen</Value>
</LocaleResource>
@ -95,17 +93,17 @@ Bei einer echten Transaktion können wir daher von einer korrekten Abwicklung au
<Value>WebHook-Url</Value>
</LocaleResource>
<LocaleResource Name="NoteRefund">
<Value>Es wurde eine Rückerstattung vorgenommen. Bitte folgen Sie diesem Link:</Value>
</LocaleResource>
<Value>Es wurde eine Rückerstattung vorgenommen. Bitte folgen Sie diesem Link:</Value>
</LocaleResource>
<LocaleResource Name="PaymentInfo">
<Value>Nach Abschluss Ihrer Bestellung werden Sie an unseren BTCPay Server weitergeleitet, wo Sie die Zahlung für Ihre Bestellung tätigen können..</Value>
</LocaleResource>
<LocaleResource Name="PaymentInfo">
<Value>Nach Abschluss Ihrer Bestellung werden Sie an unseren BTCPay Server weitergeleitet, wo Sie die Zahlung für Ihre Bestellung tätigen können..</Value>
</LocaleResource>
<LocaleResource Name="PaymentError">
<Value>Fehler bei der Verarbeitung der Zahlung. Bitte versuchen Sie es erneut und kontaktieren Sie uns, wenn das Problem weiterhin besteht.</Value>
</LocaleResource>
</Children>
</LocaleResource>
</Children>
</LocaleResource>
</Language>

View File

@ -60,9 +60,9 @@
<div class="mb-1">The plugin configuration can be done automatically or manually.</div>
<div class="mb-1"><br/><b>Automatic Configuration:</b></div>
<ul>
<li>Enter the "BTCPay Url" parameter and save.</li>
<li>Click on the "Create API Key Automatically" link to be redirected to the key creation page on your BTCPay server.</li>
<li>The "API Key" and "BTCPay Store ID" parameters will be automatically filled. Save.</li>
<li>Enter the "BTCPay Url" parameter.</li>
<li>Click on the "Configure automatically" button to be redirected to the key creation page on your BTCPay server.</li>
<li>The "API Key", "BTCPay Store ID" and "WebHook Secret" parameters will be automatically filled. Save.</li>
</ul>
<div class="mb-1"><br/><b>Manual Configuration:</b></div>
<ul>
@ -96,7 +96,7 @@ With a real transaction, you can therefore expect correct operation.
<LocaleResource Name="PaymentInfo">
<Value>After completing the order you will be redirected to the merchant BTCPay instance, where you can make the Bitcoin payment for your order.</Value>
</LocaleResource>
<LocaleResource Name="PaymentError">
<Value>Error processing the payment. Please try again and contact us if the problem persists.</Value>
</LocaleResource>

View File

@ -24,11 +24,9 @@
<div class="mb-1">La configuration du plugin peut se faire de manière automatique ou manuelle.</div>
<div class="mb-1"><br/><b>Configuration automatique :</b></div>
<ul>
<li>Renseignez le paramètre "BTCPay Url" et sauvegardez</li>
<li>Cliquez sur le lien "Créer la clé API automatiquement" pour être redirigé vers la page de création de la clé sur votre serveur BTCPay Server</li>
<li>Les paramètres "Clé API" et "BTCPay Store ID" sont alors automatiquement renseignés. Sauvegardez</li>
<li>Cliquez sur le lien "Créer le WebHook automatiquement"</li>
<li>Le champ "WebHook Secret" est alors automatiquement renseigné. Sauvegardez</li>
<li>Renseignez le paramètre "BTCPay Url"</li>
<li>Cliquez sur le bouton "Configure automatically" pour être redirigé vers la page de création de la clé sur votre serveur BTCPay Server</li>
<li>Les paramètres "Clé API", "BTCPay Store ID" et "WebHook Secret" sont alors automatiquement renseignés. Sauvegardez</li>
</ul>
<div class="mb-1"><br/><b>Configuration manuelle :</b></div>
<ul>
@ -55,7 +53,7 @@ C'est parce que BTCPay envoie des données vides alors que le plugin SmartStore
Cette erreur indique donc que le webhook est bien accessible depuis BTCPay.<br/>
Avec une vraie transaction, on peut donc s'attendre à un fonctionnement correct.
]]>
</Value>
</Value>
</LocaleResource>
<LocaleResource Name="NoteRefund">

View File

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Smartstore.BTCPayServer.Configuration;
using Smartstore.Core.Common;
using Smartstore.Core.Data;
using Smartstore.Engine.Modularity;
using Smartstore.Http;
@ -8,19 +10,48 @@ namespace Smartstore.BTCPayServer
{
internal class Module : ModuleBase, IConfigurable
{
private readonly SmartDbContext _smartDbContext;
public Module(SmartDbContext smartDbContext)
{
_smartDbContext = smartDbContext;
}
public override async Task InstallAsync(ModuleInstallationContext context)
{
await SaveSettingsAsync(new BtcPaySettings
{
BtcPayUrl = "",
ApiKey = "",
BtcPayStoreID = "",
WebHookSecret = ""
BtcPayUrl = "", ApiKey = "", BtcPayStoreID = "", WebHookSecret = ""
});
await ImportLanguageResourcesAsync();
await AddBTCCurrency(_smartDbContext);
await base.InstallAsync(context);
}
private async Task AddBTCCurrency(SmartDbContext smartDbContext)
{
try
{
await smartDbContext.Currencies.AddAsync(new Currency
{
DisplayLocale = "en-US",
Name = "Bitcoin",
CurrencyCode = "BTC",
CustomFormatting = "{0} ₿",
Published = true,
RoundNumDecimals = 8,
DisplayOrder = 1,
});
await smartDbContext.SaveChangesAsync();
}
catch (Exception e)
{
// ignored
}
}
public override async Task UninstallAsync()
{
await DeleteSettingsAsync<BtcPaySettings>();
@ -33,6 +64,6 @@ namespace Smartstore.BTCPayServer
public RouteInfo GetConfigurationRoute()
=> new("Configure", "BtcPayAdmin", new { area = "Admin" });
=> new("Configure", "BtcPayAdmin", new {area = "Admin"});
}
}
}

View File

@ -13,6 +13,7 @@ using Smartstore.Core;
using Smartstore.Core.Checkout.Cart;
using Smartstore.Core.Checkout.Orders;
using Smartstore.Core.Checkout.Payment;
using Smartstore.Core.Common;
using Smartstore.Core.Common.Services;
using Smartstore.Core.Configuration;
using Smartstore.Core.Data;
@ -27,7 +28,7 @@ namespace Smartstore.BTCPayServer.Providers
[SystemName("Smartstore.BTCPayServer")]
[FriendlyName("BTCPayServer")]
[Order(1)]
public class BTCPayPaymentProvider : PaymentMethodBase, IConfigurable
public class BTCPayPaymentProvider : PaymentMethodBase, IConfigurable, IExchangeRateProvider
{
// https://smartstore.atlassian.net/wiki/spaces/SMNET40/pages/1927643267/How+to+write+a+Payment+Plugin
@ -132,31 +133,35 @@ namespace Smartstore.BTCPayServer.Providers
Customer? myCustomer = await _customerService.GetAuthenticatedCustomerAsync();
if (myCustomer == null)
{
myCustomer = await _db.Customers.FirstOrDefaultAsync(x => x.Id == processPaymentRequest.CustomerId)
?? throw new Exception("Customer not found");
myCustomer = await _db.Customers.Include(customer => customer.BillingAddress)
.FirstOrDefaultAsync(x => x.Id == processPaymentRequest.CustomerId)
?? throw new Exception("Customer not found");
sEmail = myCustomer.BillingAddress?.Email;
sFullName = myCustomer.BillingAddress?.GetFullName();
} else
sFullName = myCustomer.BillingAddress?.GetFullName();
}
else
{
sEmail = myCustomer.Email;
sFullName = myCustomer.FullName;
}
var invoice = await _btcPayService.CreateInvoice(settings, new PaymentDataModel()
{
CurrencyCode = _currencyService.PrimaryCurrency.CurrencyCode,
Amount = processPaymentRequest.OrderTotal,
BuyerEmail = sEmail,
BuyerName = sFullName,
OrderID = processPaymentRequest.OrderGuid.ToString(),
StoreID = myStore.Id,
CustomerID = myCustomer.Id,
Description = "From " + myStore.Name,
RedirectionURL = myStore.Url + "checkout/completed",
Lang = _services.WorkContext.WorkingLanguage.LanguageCulture,
OrderUrl = new Uri(new Uri(myStore.Url), _linkGenerator.GetPathByAction("Details", "Order", new {orderId = processPaymentRequest.OrderGuid.ToString()})).ToString()
}) ;
var invoice = await _btcPayService.CreateInvoice(settings,
new PaymentDataModel()
{
CurrencyCode = _currencyService.PrimaryCurrency.CurrencyCode,
Amount = processPaymentRequest.OrderTotal,
BuyerEmail = sEmail,
BuyerName = sFullName,
OrderID = processPaymentRequest.OrderGuid.ToString(),
StoreID = myStore.Id,
CustomerID = myCustomer.Id,
Description = "From " + myStore.Name,
RedirectionURL = myStore.Url + "checkout/completed",
Lang = _services.WorkContext.WorkingLanguage.LanguageCulture,
OrderUrl = new Uri(new Uri(myStore.Url),
_linkGenerator.GetPathByAction("Index", "BtcpayOrder",
new {id = processPaymentRequest.OrderGuid})).ToString()
});
result.AuthorizationTransactionResult = invoice.CheckoutLink;
result.AuthorizationTransactionId = invoice.Id;
@ -275,5 +280,22 @@ namespace Smartstore.BTCPayServer.Providers
return new VoidPaymentResult() {NewPaymentStatus = request.Order.PaymentStatus};
}
}
public async Task<IList<ExchangeRate>> GetCurrencyLiveRatesAsync(string exchangeRateCurrencyCode)
{
var updateDate = DateTime.UtcNow;
var currencies = await _db.Currencies.ToListAsync();
var myStore = _services.StoreContext.CurrentStore;
var settings = _settingFactory.LoadSettings<BtcPaySettings>(myStore.Id);
var client = _btcPayService.GetClient(settings);
var pairs = currencies.Select(currency => exchangeRateCurrencyCode + "_" + currency.CurrencyCode).ToArray();
var rates = await client.GetStoreRates(settings.BtcPayStoreID,pairs);
return rates.Where(result => result.Rate is not null).Select(result => new ExchangeRate()
{
CurrencyCode = result.CurrencyPair.Split("_")[1],
Rate = result.Rate.Value,
UpdatedOn = updateDate
}).ToList();
}
}
}

View File

@ -1,34 +1,42 @@
# BTCPay Plugin for SmartStore
# BTCPay plugin for SmartStore - accept Bitcoin payments
![BTCPay Server](https://github.com/btcpayserver/btcpayserver/raw/master/BTCPayServer/wwwroot/img/btc_pay_BG_twitter.png)
![BTCPay Server SmartStore banner](https://raw.githubusercontent.com/btcpayserver/Smartstore.BTCPayServer/main/src/Smartstore.Modules/Smartstore.BTCPayServer/wwwroot/banner.png)
## Plugin Overview
This plugin allows you to easily integrate Bitcoin payments into your SmartStore website using BTCPay Server. You can configure the plugin either automatically or manually, depending on your preferences and requirements.
This plugin allows you to easily integrate Bitcoin payments into your SmartStore website using BTCPay Server — a free, self-hosted and open-source payment gateway solution designed to revolutionize Bitcoin payments. Our seamless integration with SmartStore ensures a hassle-free connection to your self-hosted BTCPay Server.
Experience the simplicity of accepting Bitcoin payments with just a few straightforward steps. You can configure the plugin either automatically or manually, depending on your preferences and requirements.
## Automatic Configuration
1. Enter the "BTCPay Url" parameter and save.2. Click on the "Create API Key Automatically" link to be redirected to the key creation page on your BTCPay server.
3. The "API Key" and "BTCPay Store ID" parameters will be automatically filled and a webhook will be automatically created. Save.
1. Enter Url to your BTCPay Server into "BTCPay Url" field. (e.g. https://mainnet.demo.btcpayserver.org)
2. Click on the "Configure automatically" button to be redirected to the API authorization page on your BTCPay server
3. On BTCPay authorization page: Select the store you want to connect to your Smartstore (you might need to login first)
4. Click on "Authorize App" button and you will be redirected back to your Smartstore
3. The "API Key", "BTCPay Store ID" and "Webhook Secret" fields will be automatically filled and a webhook created
4. Click "Save" button at the top right to persist the configuration
5. Congrats, the configuration is now done.
## Manual Configuration
Ensure that the following fields are filled out: "BTCPay Url," "API Key," "BTCPay Store ID," and "WebHook Secret."
To create the BTCPay API key, [read this](https://docs.btcpayserver.org/VirtueMart/#22-create-an-api-key-and-configure-permissions).
- Note: If you want to use the Refund feature, you must also add the "Modify your stores" permission. After a refund, an order note is created, indicating the BTCPay link where the customer can request a refund.
- Note: If you want to use the Refund feature, you must also add the "Create non-approved pull payments" permission. After a refund, an order note is created where you can copy the pull payments link and send to your customer. The customer can request the refund on that page.
To create the BTCPay WebHook, [read this](https://docs.btcpayserver.org/VirtueMart/#23-create-a-webhook-on-btcpay-server) and use the default secret code generated by BTCPay.
- Note: Other than in the guide you need to copy the Url shown in field "Webhook Url" from your configuration screen on Smartstore.
## Support and Documentation
## Support
For more information and detailed instructions, please visit the [official documentation](https://docs.btcpayserver.org/).
Feel free to join our support channel over at [Mattermost Chat](https://chat.btcpayserver.org/) if you need help or have any further questions.
If experience a bug please open an issue in our repository [here](https://github.com/btcpayserver/Smartstore.BTCPayServer/issues)
## License
This plugin is released under the [MIT License](LICENSE).
---
For more information and updates, visit our [GitHub repository](https://github.com/Nisaba/btcpay-smartstore-plugin).
Find our latest releases on the [Smartstore marketplace](https://community.smartstore.com/index.php?/files/file/246-btcpay-server-for-smartstore/) or on our [release page](https://github.com/btcpayserver/Smartstore.BTCPayServer/releases)

View File

@ -11,13 +11,11 @@ using Smartstore.Core.Checkout.Payment;
namespace Smartstore.BTCPayServer.Services
{
public class BtcPayService
{
private readonly IHttpClientFactory _httpClientFactory;
public BtcPayService(IHttpClientFactory httpClientFactory )
public BtcPayService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
@ -25,12 +23,13 @@ namespace Smartstore.BTCPayServer.Services
public BTCPayServerClient GetClient(BtcPaySettings settings)
{
return new BTCPayServerClient(new Uri(settings.BtcPayUrl), settings.ApiKey,_httpClientFactory.CreateClient("BTCPayServer"));
return new BTCPayServerClient(new Uri(settings.BtcPayUrl), settings.ApiKey,
_httpClientFactory.CreateClient("BTCPayServer"));
}
public async Task<string> GetStoreId(BtcPaySettings settings)
{
return (await GetClient(settings).GetStores()).First().Id;
return (await GetClient(settings).GetStores()).First().Id;
}
public static bool CheckSecretKey(string key, string message, string signature)
@ -42,7 +41,6 @@ namespace Smartstore.BTCPayServer.Services
public async Task<InvoiceData> CreateInvoice(BtcPaySettings settings, PaymentDataModel paymentData)
{
var client = GetClient(settings);
var req = new CreateInvoiceRequest()
{
@ -55,7 +53,7 @@ namespace Smartstore.BTCPayServer.Services
RedirectAutomatically = true,
RequiresRefundEmail = false
},
Metadata = JObject.FromObject(new
Metadata = JObject.FromObject(new
{
buyerEmail = paymentData.BuyerEmail,
buyerName = paymentData.BuyerName,
@ -63,18 +61,15 @@ namespace Smartstore.BTCPayServer.Services
orderUrl = paymentData.OrderUrl,
itemDesc = paymentData.Description,
}),
Receipt = new InvoiceDataBase.ReceiptOptions()
{
Enabled = true,
}
Receipt = new InvoiceDataBase.ReceiptOptions() {Enabled = true,}
};
var invoice = await client.CreateInvoice(settings.BtcPayStoreID, req);
return invoice;
}
public async Task<string> CreateRefund(BtcPaySettings settings, RefundPaymentRequest refundRequest)
{
var client = GetClient(settings);
var invoice = await client.GetInvoicePaymentMethods(settings.BtcPayStoreID,
refundRequest.Order.AuthorizationTransactionId);
@ -102,33 +97,46 @@ namespace Smartstore.BTCPayServer.Services
refundRequest.Order.AuthorizationTransactionId, refundInvoiceRequest);
return refund.ViewLink;
}
public async Task<string> CreateWebHook(BtcPaySettings settings, string WebHookUrl)
public async Task<string> CreateWebHook(BtcPaySettings settings, string webHookUrl)
{
var client = GetClient(settings);
var response = await client.CreateWebhook(settings.BtcPayStoreID, new CreateStoreWebhookRequest()
var existing = await client.GetWebhooks(settings.BtcPayStoreID);
var existingWebHook = existing.Where(x => x.Url == webHookUrl);
foreach (var webhookData in existingWebHook)
{
Url = WebHookUrl,
Enabled = true,
});
await client.DeleteWebhook(settings.BtcPayStoreID, webhookData.Id);
}
var response = await client.CreateWebhook(settings.BtcPayStoreID,
new CreateStoreWebhookRequest()
{
Url = webHookUrl,
Enabled = true,
AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData()
{
SpecificEvents = new[]
{
WebhookEventType.InvoiceReceivedPayment, WebhookEventType.InvoiceProcessing,
WebhookEventType.InvoiceExpired, WebhookEventType.InvoiceSettled,
WebhookEventType.InvoiceInvalid, WebhookEventType.InvoicePaymentSettled,
}
}
});
return response.Secret;
}
public async Task<InvoiceData> GetInvoice(BtcPaySettings settings, string invoiceId)
{
var client = GetClient(settings);
return await client.GetInvoice(settings.BtcPayStoreID, invoiceId);
}
public async Task<bool> UpdateOrderWithInvoice(BtcPaySettings settings ,Order order, string invoiceId)
public async Task<bool> UpdateOrderWithInvoice(BtcPaySettings settings, Order order, string invoiceId)
{
try
{
var invoice = await GetInvoice(settings, invoiceId);
return await UpdateOrderWithInvoice(order, invoice, null);
}
@ -141,7 +149,7 @@ namespace Smartstore.BTCPayServer.Services
return true;
}
}
public async Task<bool> UpdateOrderWithInvoice(Order order, InvoiceData invoiceData,
WebhookInvoiceEvent? webhookEvent)
{
@ -158,7 +166,7 @@ namespace Smartstore.BTCPayServer.Services
newPaymentStatus = PaymentStatus.Pending;
break;
case InvoiceStatus.Processing:
newPaymentStatus =PaymentStatus.Pending; // PaymentStatus.Authorized; smartstore will set the order to processing otherwise
newPaymentStatus = PaymentStatus.Authorized;
newOrderStatus = OrderStatus.Pending;
break;
case InvoiceStatus.Expired:
@ -191,40 +199,53 @@ namespace Smartstore.BTCPayServer.Services
updated = true;
}
var additionalMessage = GetAdditionalMessageFromWebhook(webhookEvent);
if (updated)
{
var aditionalData = GetAdditionalMessageFromWebhook(webhookEvent)?.message;
aditionalData = string.IsNullOrEmpty(aditionalData) ? "" : $" - {aditionalData}";
additionalMessage = string.IsNullOrEmpty(additionalMessage) ? "" : $" - {additionalMessage}";
order.AddOrderNote(
$"BTCPayServer: Order status updated to {newOrderStatus} and payment status to {newPaymentStatus} by BTCPay with invoice {invoiceData.Id}{aditionalData}",
$"BTCPayServer: Order status updated to {newOrderStatus} and payment status to {newPaymentStatus} by BTCPay with invoice {invoiceData.Id}{additionalMessage}",
false);
order.HasNewPaymentNotification = true;
if (order.PaymentStatus == PaymentStatus.Paid && !string.IsNullOrEmpty(order.CaptureTransactionResult))
if (order.PaymentStatus is PaymentStatus.Authorized or PaymentStatus.Paid &&
!string.IsNullOrEmpty(order.CaptureTransactionResult))
order.AddOrderNote(
$"BTCPayServer: Payment received. <a href='{order.CaptureTransactionResult}'>Click here for more information.</a>", true);
$"BTCPayServer: Payment received {(order.PaymentStatus is PaymentStatus.Authorized ? $"but waiting to confirm. <a href='{order.AuthorizationTransactionResult}'>Click here for more information.</a>" : $". <a href='{order.CaptureTransactionResult}'>Click here for more information.</a>")}", true);
return true;
}
if (!string.IsNullOrEmpty(additionalMessage))
{
order.AddOrderNote(
$"BTCPayServer: {additionalMessage}", false);
}
return false;
}
private (string message, bool customerFriendly)? GetAdditionalMessageFromWebhook(WebhookInvoiceEvent? webhookEvent)
private string? GetAdditionalMessageFromWebhook(WebhookInvoiceEvent? webhookEvent)
{
switch (webhookEvent?.Type)
{
case WebhookEventType.InvoiceReceivedPayment when webhookEvent.ReadAs<WebhookInvoiceReceivedPaymentEvent>() is { } receivedPaymentEvent:
return ($"Payment detected ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})" , false);
case WebhookEventType.InvoicePaymentSettled when webhookEvent.ReadAs<WebhookInvoicePaymentSettledEvent>() is { } receivedPaymentEvent:
return ($"Payment settled ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})" , false);
case WebhookEventType.InvoiceProcessing when webhookEvent.ReadAs<WebhookInvoiceProcessingEvent>() is { } receivedPaymentEvent && receivedPaymentEvent.OverPaid:
return ($"Invoice was overpaid." , false);
case WebhookEventType.InvoiceExpired when webhookEvent.ReadAs<WebhookInvoiceExpiredEvent>() is { } receivedPaymentEvent && receivedPaymentEvent.PartiallyPaid:
return ($"Invoice expired but was paid partially, please check." , false);
default: return null;
}
switch (webhookEvent?.Type)
{
case WebhookEventType.InvoiceReceivedPayment
when webhookEvent.ReadAs<WebhookInvoiceReceivedPaymentEvent>() is { } receivedPaymentEvent:
return
$"Payment detected ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})";
case WebhookEventType.InvoicePaymentSettled
when webhookEvent.ReadAs<WebhookInvoicePaymentSettledEvent>() is { } receivedPaymentEvent:
return
$"Payment settled ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})";
case WebhookEventType.InvoiceProcessing
when webhookEvent.ReadAs<WebhookInvoiceProcessingEvent>() is { } receivedPaymentEvent &&
receivedPaymentEvent.OverPaid:
return $"Invoice was overpaid.";
case WebhookEventType.InvoiceExpired
when webhookEvent.ReadAs<WebhookInvoiceExpiredEvent>() is { } receivedPaymentEvent &&
receivedPaymentEvent.PartiallyPaid:
return $"Invoice expired but was paid partially.";
default: return null;
}
}
}
}

View File

@ -139,27 +139,30 @@
<span asp-validation-for="WebHookSecret" />
</div>
</div>
<span></span>
<br/>
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="AdditionalFee" />
<hr/>
<div class="card border-0" id="additionalOptions">
<h6 class="card-title" >Additional Options</h6>
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="AdditionalFee"/>
</div>
<div class="adminData">
<editor asp-for="AdditionalFee" sm-postfix="@ViewBag.StoreCurrencyCode"/>
<span asp-validation-for="AdditionalFee"></span>
</div>
</div>
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="AdditionalFeePercentage"/>
</div>
<div class="adminData">
<editor asp-for="AdditionalFeePercentage"/>
<span asp-validation-for="AdditionalFeePercentage"></span>
</div>
</div>
</div>
<div class="adminData">
<editor asp-for="AdditionalFee" sm-postfix="@ViewBag.StoreCurrencyCode" />
<span asp-validation-for="AdditionalFee"></span>
</div>
</div>
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="AdditionalFeePercentage" />
</div>
<div class="adminData">
<editor asp-for="AdditionalFeePercentage" />
<span asp-validation-for="AdditionalFeePercentage"></span>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
"SystemName": "Smartstore.BTCPayServer",
"FriendlyName": "Bitcoin payments with BTCPay Server",
"Description": "This plugin enables Bitcoin payments with your BTCPay Server instance",
"Version": "5.0.6",
"Version": "5.0.5.1",
"MinAppVersion": "5.0.5",
"Author": "Nisaba Solutions",
"Order": 1,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,28 @@
using Smartstore.Engine.Modularity;
using Smartstore.IO;
var modulePath = args[0];
var buildPath = args[1];
var dirInfo = new DirectoryInfo(modulePath);
var lfs = new LocalFileSystem(dirInfo.ToString());
var descriptor = ModuleDescriptor.Create(new LocalDirectory("/", dirInfo,lfs), new LocalFileSystem(dirInfo.Parent.ToString()));
var pb = new Smartstore.Core.Packaging.PackageBuilder(new LocalFileSystem(dirInfo.Parent.Parent.ToString()));
var package = await pb.BuildPackageAsync(descriptor);
var fileName = package.FileName;
if (!Directory.Exists(buildPath))
{
Directory.CreateDirectory(buildPath);
}
fileName = Path.Combine(buildPath, fileName);
await using (var stream = File.Create(fileName))
{
await package.ArchiveStream.CopyToAsync(stream);
}
var fileInfo = new FileInfo(fileName);
Console.Write($"{fileInfo.FullName}");

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Smartstore.Core\Smartstore.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\build-btcpayplugin.ps1">
<Link>build-btcpayplugin.ps1</Link>
</Content>
<Content Include="..\..\build-btcpayplugin.sh">
<Link>build-btcpayplugin.sh</Link>
</Content>
</ItemGroup>
</Project>