diff --git a/BTCPayServer.Tests/Checkoutv2Tests.cs b/BTCPayServer.Tests/Checkoutv2Tests.cs
index fca082f6b..3381b28a4 100644
--- a/BTCPayServer.Tests/Checkoutv2Tests.cs
+++ b/BTCPayServer.Tests/Checkoutv2Tests.cs
@@ -186,6 +186,7 @@ namespace BTCPayServer.Tests
Assert.True(paidSection.Displayed);
Assert.Contains("Invoice Paid", paidSection.Text);
});
+ s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("ReceiptLink"));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs
index 02852ea0e..ae20411e4 100644
--- a/BTCPayServer.Tests/ThirdPartyTests.cs
+++ b/BTCPayServer.Tests/ThirdPartyTests.cs
@@ -353,6 +353,11 @@ retry:
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
+
+ actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
+ version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
+ expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
+ Assert.Equal(expected, actual);
}
string GetFileContent(params string[] path)
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index dd147d8ad..7de0b3736 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -789,6 +789,7 @@ namespace BTCPayServer.Controllers
BrandColor = storeBlob.BrandColor,
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
+ CelebratePayment = storeBlob.CelebratePayment,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs
index b54d622c2..21f83c442 100644
--- a/BTCPayServer/Controllers/UIStoresController.cs
+++ b/BTCPayServer/Controllers/UIStoresController.cs
@@ -386,6 +386,7 @@ namespace BTCPayServer.Controllers
}).ToList();
vm.UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2;
+ vm.CelebratePayment = storeBlob.CelebratePayment;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
@@ -505,7 +506,7 @@ namespace BTCPayServer.Controllers
}
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
-
+ blob.CelebratePayment = model.CelebratePayment;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs
index 71ae55bd3..7150cbea2 100644
--- a/BTCPayServer/Data/StoreBlob.cs
+++ b/BTCPayServer/Data/StoreBlob.cs
@@ -218,6 +218,10 @@ namespace BTCPayServer.Data
public string BrandColor { get; set; }
public string LogoFileId { get; set; }
public string CssFileId { get; set; }
+
+ [DefaultValue(true)]
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
+ public bool CelebratePayment { get; set; } = true;
public IPaymentFilter GetExcludedPaymentMethods()
{
diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs
index b24383110..ae3476832 100644
--- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs
+++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs
@@ -33,6 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels
public bool IsModal { get; set; }
public bool IsUnsetTopUp { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
+ public bool CelebratePayment { get; set; }
public string CryptoCode { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
index 70aba10d4..674ccb652 100644
--- a/BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
+++ b/BTCPayServer/Models/StoreViewModels/CheckoutAppearanceViewModel.cs
@@ -35,6 +35,9 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Use the new checkout")]
public bool UseNewCheckout { get; set; }
+ [Display(Name = "Celebrate payment with confetti")]
+ public bool CelebratePayment { get; set; }
+
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail { get; set; }
diff --git a/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml b/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
index 907b00482..07ee61027 100644
--- a/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
+++ b/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
@@ -104,6 +104,7 @@
+
@@ -130,7 +131,7 @@
-
+
@@ -148,7 +149,7 @@
@@ -219,13 +220,18 @@
+ @if (Model.CelebratePayment)
+ {
+
+ }
diff --git a/BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml b/BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
index 68a98084c..4a9b5eccf 100644
--- a/BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
+++ b/BTCPayServer/Views/UIStores/CheckoutAppearance.cshtml
@@ -86,6 +86,10 @@
+
+
+
+
diff --git a/BTCPayServer/wwwroot/checkout-v2/checkout.css b/BTCPayServer/wwwroot/checkout-v2/checkout.css
index ff858cdb5..12e88a31e 100644
--- a/BTCPayServer/wwwroot/checkout-v2/checkout.css
+++ b/BTCPayServer/wwwroot/checkout-v2/checkout.css
@@ -4,6 +4,9 @@
--border-radius: var(--btcpay-border-radius-l);
--wrap-max-width: 400px;
}
+body {
+ overflow-x: hidden;
+}
.public-page-wrap {
flex-direction: column;
max-width: var(--wrap-max-width);
@@ -160,7 +163,13 @@ section dl > div dd {
height: 3rem;
margin: .5rem auto 1.5rem;
}
-
+#result #confetti {
+ position: absolute;
+ left: 50%;
+ top: 2rem;
+ width: 1.5rem;
+ height: 1.5rem;
+}
#result #paid .top .icn .icon {
color: var(--btcpay-primary);
}
diff --git a/BTCPayServer/wwwroot/checkout-v2/checkout.js b/BTCPayServer/wwwroot/checkout-v2/checkout.js
index 341a6bce4..ec23a0ea7 100644
--- a/BTCPayServer/wwwroot/checkout-v2/checkout.js
+++ b/BTCPayServer/wwwroot/checkout-v2/checkout.js
@@ -183,6 +183,28 @@ function initApp() {
return !this.paymentMethodIds.includes(this.pmId);
}
},
+ watch: {
+ isPaid: function (newValue, oldValue) {
+ if (newValue === true && oldValue === false) {
+ const duration = 5000;
+ const self = this;
+ // celebration!
+ Vue.nextTick(function () {
+ self.celebratePayment(duration);
+ });
+ // automatic redirect or close
+ if (self.srvModel.redirectAutomatically && self.storeLink) {
+ setTimeout(function () {
+ if (self.isModal && window.top.location === self.storeLink) {
+ self.close();
+ } else {
+ window.top.location = self.storeLink;
+ }
+ }, duration);
+ }
+ }
+ }
+ },
mounted () {
this.updateData(this.srvModel);
this.updateTimer();
@@ -268,20 +290,23 @@ function initApp() {
// updating ui
this.srvModel = data;
eventBus.$emit('data-fetched', this.srvModel);
-
- const self = this;
- if (self.isPaid && data.redirectAutomatically && self.storeLink) {
- setTimeout(function () {
- if (self.isModal && window.top.location === self.storeLink){
- self.close();
- } else {
- window.top.location = self.storeLink;
- }
- }, 2000);
- }
},
replaceNewlines (value) {
return value ? value.replace(/\n/ig, '
') : '';
+ },
+ async celebratePayment (duration) {
+ const $confettiEl = document.getElementById('confetti')
+ if (window.confetti && $confettiEl && !$confettiEl.dataset.running) {
+ $confettiEl.dataset.running = true;
+ await window.confetti($confettiEl, {
+ duration,
+ spread: 90,
+ stagger: 5,
+ elementCount: 121,
+ colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"]
+ });
+ delete $confettiEl.dataset.running;
+ }
}
}
});
diff --git a/BTCPayServer/wwwroot/vendor/dom-confetti/dom-confetti.min.js b/BTCPayServer/wwwroot/vendor/dom-confetti/dom-confetti.min.js
new file mode 100644
index 000000000..5bf3be974
--- /dev/null
+++ b/BTCPayServer/wwwroot/vendor/dom-confetti/dom-confetti.min.js
@@ -0,0 +1,8 @@
+/**
+ * Minified by jsDelivr using Terser v3.14.1.
+ * Original file: /npm/dom-confetti@0.2.2/lib/main.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.confetti=confetti;var defaultColors=["#a864fd","#29cdff","#78ff44","#ff718d","#fdff6a"];function createElements(e,t,i,s,n){return Array.from({length:t}).map(function(t,a){var r=document.createElement("div"),l=i[a%i.length];return r.style["background-color"]=l,r.style.width=s,r.style.height=n,r.style.position="absolute",r.style.willChange="transform, opacity",r.style.visibility="hidden",e.appendChild(r),r})}function randomPhysics(e,t,i,s){var n=e*(Math.PI/180),a=t*(Math.PI/180);return{x:0,y:0,z:0,wobble:10*s(),wobbleSpeed:.1+.1*s(),velocity:.5*i+s()*i,angle2D:-n+(.5*a-s()*a),angle3D:-Math.PI/4+s()*(Math.PI/2),tiltAngle:s()*Math.PI,tiltAngleSpeed:.1+.3*s()}}function updateFetti(e,t,i,s){e.physics.x+=Math.cos(e.physics.angle2D)*e.physics.velocity,e.physics.y+=Math.sin(e.physics.angle2D)*e.physics.velocity,e.physics.z+=Math.sin(e.physics.angle3D)*e.physics.velocity,e.physics.wobble+=e.physics.wobbleSpeed,s?e.physics.velocity*=s:e.physics.velocity-=e.physics.velocity*i,e.physics.y+=3,e.physics.tiltAngle+=e.physics.tiltAngleSpeed;var n=e.physics,a=n.x,r=n.y,l=n.z,o=n.tiltAngle,c=n.wobble,h="translate3d("+(a+10*Math.cos(c))+"px, "+(r+10*Math.sin(c))+"px, "+l+"px) rotate3d(1, 1, 1, "+o+"rad)";e.element.style.visibility="visible",e.element.style.transform=h,e.element.style.opacity=1-t}function animate(e,t,i,s,n,a){var r=void 0;return new Promise(function(l){requestAnimationFrame(function o(c){r||(r=c);var h=c-r,y=r===c?0:(c-r)/n;t.slice(0,Math.ceil(h/a)).forEach(function(e){updateFetti(e,y,i,s)}),c-r1&&void 0!==arguments[1]?arguments[1]:{},i=Object.assign({},defaults,backwardPatch(t)),s=i.elementCount,n=i.colors,a=i.width,r=i.height,l=i.perspective,o=i.angle,c=i.spread,h=i.startVelocity,y=i.decay,d=i.dragFriction,p=i.duration,u=i.stagger,f=i.random;return e.style.perspective=l,animate(e,createElements(e,s,n,a,r).map(function(e){return{element:e,physics:randomPhysics(o,c,h,f)}}),d,y,p,u)}
+//# sourceMappingURL=/sm/b04c41c9e0ca7d063aa88138fff144a5dd6481a3809187211290de788b6184f8.map