mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
Checkout v2: Celebrate payment with confetti (#4727)
* Checkout v2: Celebrate payment with confetti Have a colorful celebration for successful payments. * Make it default and add test
This commit is contained in:
parent
eb3ba95114
commit
f3d9e07c5e
12 changed files with 84 additions and 16 deletions
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
<div id="paid" v-if="isPaid">
|
||||
<div class="top">
|
||||
<span class="icn">
|
||||
<div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div>
|
||||
<vc:icon symbol="payment-complete"/>
|
||||
</span>
|
||||
<h4 v-t="'invoice_paid'"></h4>
|
||||
|
@ -130,7 +131,7 @@
|
|||
<div id="expired" v-if="isUnpayable">
|
||||
<div class="top">
|
||||
<span class="icn">
|
||||
<vc:icon symbol="invoice-expired"/>
|
||||
<vc:icon symbol="invoice-expired" />
|
||||
</span>
|
||||
<h4 v-t="'invoice_expired'"></h4>
|
||||
<div id="PaymentDetails" class="payment-details">
|
||||
|
@ -148,7 +149,7 @@
|
|||
</div>
|
||||
<button class="d-flex align-items-center gap-1 btn btn-link payment-details-button" type="button" :aria-expanded="displayPaymentDetails ? 'true' : 'false'" v-on:click="displayPaymentDetails = !displayPaymentDetails">
|
||||
<span class="fw-semibold" v-t="'view_details'"></span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
<vc:icon symbol="caret-down" />
|
||||
</button>
|
||||
<p class="text-center mt-3" v-html="replaceNewlines($t('invoice_expired_body', { storeName: srvModel.storeName, minutes: @Model.MaxTimeMinutes }))"></p>
|
||||
</div>
|
||||
|
@ -219,13 +220,18 @@
|
|||
</dl>
|
||||
</script>
|
||||
<script>
|
||||
const i18nUrl = @Safe.Json($"{Model.RootPath}misc/translations/checkout-v2/{{{{lng}}}}?v={Env.Version}");
|
||||
const i18nUrl = @Safe.Json($"{Model.RootPath}misc/translations/checkout-v2/{{{{lng}}}}?v={Env.Version}");
|
||||
const statusUrl = @Safe.Json(Url.Action("GetStatus", new { invoiceId = Model.InvoiceId }));
|
||||
const statusWsUrl = @Safe.Json(Url.Action("GetStatusWebSocket", new { invoiceId = Model.InvoiceId }));
|
||||
const availableLanguages = @Safe.Json(LangService.GetLanguages().Select(language => language.Code));
|
||||
const initialSrvModel = @Safe.Json(Model);
|
||||
const qrOptions = { margin: 0, type: 'svg', color: { dark: '#000', light: '#fff' } };
|
||||
window.exports = {};
|
||||
</script>
|
||||
@if (Model.CelebratePayment)
|
||||
{
|
||||
<script src="~/vendor/dom-confetti/dom-confetti.min.js" asp-append-version="true"></script>
|
||||
}
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/i18next/i18next.min.js" asp-append-version="true"></script>
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
</div>
|
||||
<span asp-validation-for="DisplayExpirationTimer" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input asp-for="CelebratePayment" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="CelebratePayment" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input" />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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, '<br>') : '';
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
8
BTCPayServer/wwwroot/vendor/dom-confetti/dom-confetti.min.js
vendored
Normal file
8
BTCPayServer/wwwroot/vendor/dom-confetti/dom-confetti.min.js
vendored
Normal file
|
@ -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-r<n?requestAnimationFrame(o):(t.forEach(function(t){if(t.element.parentNode===e)return e.removeChild(t.element)}),l())})})}var defaults={angle:90,spread:45,startVelocity:45,elementCount:50,width:"10px",height:"10px",perspective:"",colors:defaultColors,duration:3e3,stagger:0,dragFriction:.1,random:Math.random};function backwardPatch(e){return!e.stagger&&e.delay&&(e.stagger=e.delay),e}function confetti(e){var t=arguments.length>1&&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
|
Loading…
Add table
Reference in a new issue