mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
249b991185
This is mostly for the POS, where either an individual item title might be the description, or in case of the cart and keypad, the name of the app will be displayed. The description is omitted if it matches the store name, to avoid duplicate titles. Closes #5023.
328 lines
20 KiB
Plaintext
328 lines
20 KiB
Plaintext
@using BTCPayServer.Services
|
|
@using BTCPayServer.Abstractions.Contracts
|
|
@inject LanguageService LangService
|
|
@inject BTCPayServerEnvironment Env
|
|
@inject IEnumerable<IUIExtension> UiExtensions
|
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
|
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
|
@model PaymentModel
|
|
@{
|
|
Layout = null;
|
|
ViewData["Title"] = Model.HtmlTitle;
|
|
ViewData["StoreBranding"] = Model.StoreBranding;
|
|
Csp.UnsafeEval();
|
|
var hasPaymentPlugins = UiExtensions.Any(extension => extension.Location == "checkout-payment-method");
|
|
var displayedPaymentMethods = Model.AvailableCryptos.Where(c => c.Displayed).ToList();
|
|
}
|
|
@functions {
|
|
private string ToJsValue(object value)
|
|
{
|
|
return Safe.Json(value).ToString()?.Replace("\"", "'");
|
|
}
|
|
}
|
|
<!DOCTYPE html>
|
|
<html lang="@Model.DefaultLang" class="@(Model.IsModal ? "checkout-modal" : "")"@(Env.IsDeveloping ? " data-devenv" : "")>
|
|
<head>
|
|
<partial name="LayoutHead"/>
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<link href="~/checkout/checkout.css" asp-append-version="true" rel="stylesheet" />
|
|
@if (!string.IsNullOrEmpty(Model.PaymentSoundUrl))
|
|
{
|
|
<link rel="preload" href="@Model.PaymentSoundUrl" as="audio" />
|
|
}
|
|
@if (!string.IsNullOrEmpty(Model.NfcReadSoundUrl))
|
|
{
|
|
<link rel="preload" href="@Model.NfcReadSoundUrl" as="audio" />
|
|
}
|
|
@if (!string.IsNullOrEmpty(Model.ErrorSoundUrl))
|
|
{
|
|
<link rel="preload" href="@Model.ErrorSoundUrl" as="audio" />
|
|
}
|
|
</head>
|
|
<body class="min-vh-100">
|
|
<div id="Checkout" class="public-page-wrap" v-cloak>
|
|
@if (Model.ShowStoreHeader)
|
|
{
|
|
<partial name="_StoreHeader" model="(Model.StoreName, Model.StoreBranding)" />
|
|
}
|
|
<main class="tile">
|
|
<nav v-if="isModal">
|
|
<button type="button" v-if="isModal" id="close" v-on:click="close">
|
|
<vc:icon symbol="close"/>
|
|
</button>
|
|
</nav>
|
|
<section id="payment" v-if="isActive">
|
|
<div v-if="srvModel.itemDesc && srvModel.itemDesc !== srvModel.storeName" v-text="srvModel.itemDesc" class="fw-semibold text-center text-muted mb-3"></div>
|
|
<div class="d-flex justify-content-center mt-1 text-center">
|
|
@if (Model.IsUnsetTopUp)
|
|
{
|
|
<h2 id="AmountDue" v-t="'any_amount'"></h2>
|
|
}
|
|
else
|
|
{
|
|
<h2 id="AmountDue" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`" :data-clipboard="asNumber(srvModel.btcDue)" data-clipboard-hover :data-amount-due="srvModel.btcDue">@Model.BtcDue @Model.CryptoCode</h2>
|
|
}
|
|
</div>
|
|
<div id="PaymentInfo" class="info mt-3 mb-2" v-collapsible="showInfo">
|
|
<div>
|
|
<div class="timer" v-if="showTimer">
|
|
<span class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden"></span></span>
|
|
<span v-t="'expiry_info'"></span> <span class="expiryTime">{{timeText}}</span>
|
|
</div>
|
|
<div class="payment-due" v-if="showPaymentDueInfo">
|
|
<vc:icon symbol="info" />
|
|
<span v-t="'partial_payment_info'"></span>
|
|
</div>
|
|
<div v-if="showPaymentDueInfo" v-html="replaceNewlines($t('still_due', { amount: `${srvModel.btcDue} ${srvModel.cryptoCode}` }))"></div>
|
|
</div>
|
|
</div>
|
|
<button id="DetailsToggle" class="d-flex align-items-center gap-1 btn btn-link payment-details-button mb-2" 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" />
|
|
</button>
|
|
<div id="PaymentDetails" class="payment-details" v-collapsible="displayPaymentDetails">
|
|
<payment-details
|
|
:srv-model="srvModel"
|
|
:is-active="isActive"
|
|
:order-amount="orderAmount"
|
|
:btc-paid="btcPaid"
|
|
:btc-due="btcDue"
|
|
:show-recommended-fee="showRecommendedFee"
|
|
class="pb-4" />
|
|
</div>
|
|
@if (displayedPaymentMethods.Count > 1 || hasPaymentPlugins)
|
|
{
|
|
<div class="mt-3 mb-2">
|
|
<h6 class="text-center mb-3" v-t="'pay_with'"></h6>
|
|
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2 pb-2">
|
|
@foreach (var crypto in displayedPaymentMethods)
|
|
{
|
|
<a asp-action="Checkout" asp-route-invoiceId="@Model.InvoiceId" asp-route-paymentMethodId="@crypto.PaymentMethodId"
|
|
class="btcpay-pill m-0 payment-method"
|
|
:class="{ active: pmId === @ToJsValue(crypto.PaymentMethodId) }"
|
|
v-on:click.prevent="changePaymentMethod(@ToJsValue(crypto.PaymentMethodId))">
|
|
@crypto.PaymentMethodName
|
|
</a>
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment-method", model = Model })
|
|
</div>
|
|
</div>
|
|
}
|
|
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
|
|
:model="srvModel"
|
|
:nfc-scanning="nfc.scanning"
|
|
:nfc-supported="nfc.supported"
|
|
:nfc-error-message="nfc.errorMessage"
|
|
v-on:start-nfc-scan="startNFCScan"
|
|
v-on:handle-nfc-data="handleNFCData"
|
|
v-on:handle-nfc-error="handleNFCError"
|
|
v-on:handle-nfc-result="handleNFCResult" />
|
|
</section>
|
|
<section id="result" v-else>
|
|
<div v-if="isProcessing" id="processing" key="processing">
|
|
<div class="top">
|
|
<span class="icn">
|
|
<div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div>
|
|
<vc:icon symbol="checkout-sent" />
|
|
</span>
|
|
<h4 v-t="'payment_received'" class="mb-4"></h4>
|
|
<p class="text-center" v-t="'payment_received_body'"></p>
|
|
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations" v-t="{ path: 'payment_received_confirmations', args: { cryptoCode: realCryptoCode, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
|
|
<div id="PaymentDetails" class="payment-details">
|
|
<dl class="mb-0">
|
|
<div>
|
|
<dt v-t="'invoice_id'"></dt>
|
|
<dd v-text="srvModel.invoiceId" :data-clipboard="srvModel.invoiceId" :data-clipboard-confirm="$t('copy_confirm')"></dd>
|
|
</div>
|
|
<div v-if="srvModel.orderId">
|
|
<dt v-t="'order_id'"></dt>
|
|
<dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" :data-clipboard-confirm="$t('copy_confirm')"></dd>
|
|
</div>
|
|
</dl>
|
|
<payment-details
|
|
:srv-model="srvModel"
|
|
:is-active="isActive"
|
|
:order-amount="orderAmount"
|
|
:btc-paid="btcPaid"
|
|
:btc-due="btcDue"
|
|
:show-recommended-fee="showRecommendedFee"
|
|
v-collapsible="displayPaymentDetails" />
|
|
</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" />
|
|
</button>
|
|
</div>
|
|
<div class="buttons mt-3" v-if="storeLink || isModal">
|
|
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
|
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
|
</div>
|
|
</div>
|
|
<div v-if="isSettled" id="settled" key="settled">
|
|
<div class="top">
|
|
<span class="icn">
|
|
<div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div>
|
|
<vc:icon symbol="checkout-complete" />
|
|
</span>
|
|
<h4 v-t="'invoice_paid'"></h4>
|
|
<div id="PaymentDetails" class="payment-details">
|
|
<dl class="mb-0">
|
|
<div>
|
|
<dt v-t="'invoice_id'"></dt>
|
|
<dd v-text="srvModel.invoiceId" :data-clipboard="srvModel.invoiceId" data-clipboard-hover="start"></dd>
|
|
</div>
|
|
<div v-if="srvModel.orderId">
|
|
<dt v-t="'order_id'"></dt>
|
|
<dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd>
|
|
</div>
|
|
</dl>
|
|
<payment-details
|
|
:srv-model="srvModel"
|
|
:is-active="isActive"
|
|
:order-amount="orderAmount"
|
|
:btc-paid="btcPaid"
|
|
:btc-due="btcDue"
|
|
:show-recommended-fee="showRecommendedFee"
|
|
class="mb-5" />
|
|
</div>
|
|
</div>
|
|
<div class="buttons" v-if="srvModel.receiptLink || storeLink || isModal">
|
|
<a v-if="srvModel.receiptLink" class="btn btn-primary rounded-pill w-100" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
|
|
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
|
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
|
</div>
|
|
</div>
|
|
<div v-if="isInvalid" id="unpaid" key="unpaid">
|
|
<div class="top">
|
|
<span class="icn">
|
|
<vc:icon symbol="checkout-expired" />
|
|
</span>
|
|
<h4 v-t="'invoice_expired'"></h4>
|
|
<div id="PaymentDetails" class="payment-details">
|
|
<dl class="mb-0">
|
|
<div>
|
|
<dt v-t="'invoice_id'"></dt>
|
|
<dd v-text="srvModel.invoiceId" :data-clipboard="srvModel.invoiceId" data-clipboard-hover="start"></dd>
|
|
</div>
|
|
<div v-if="srvModel.orderId">
|
|
<dt v-t="'order_id'"></dt>
|
|
<dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd>
|
|
</div>
|
|
</dl>
|
|
<payment-details
|
|
:srv-model="srvModel"
|
|
:is-active="isActive"
|
|
:order-amount="orderAmount"
|
|
:btc-paid="btcPaid"
|
|
:btc-due="btcDue"
|
|
:show-recommended-fee="showRecommendedFee"
|
|
v-collapsible="displayPaymentDetails" />
|
|
</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" />
|
|
</button>
|
|
<p class="text-center mt-3" v-html="replaceNewlines($t(isPaidPartial ? 'invoice_paidpartial_body' : 'invoice_expired_body', { storeName: srvModel.storeName, minutes: srvModel.maxTimeMinutes }))"></p>
|
|
</div>
|
|
<div class="buttons" v-if="(isPaidPartial && srvModel.storeSupportUrl) || storeLink || isModal">
|
|
<a v-if="isPaidPartial && srvModel.storeSupportUrl" class="btn btn-primary rounded-pill w-100" :href="srvModel.storeSupportUrl" v-t="'contact_us'" id="ContactLink"></a>
|
|
<a v-if="storeLink" class="btn btn-primary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
|
<button v-else-if="isModal" class="btn btn-primary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
@if (Env.CheatMode)
|
|
{
|
|
<checkout-cheating invoice-id="@Model.InvoiceId" :btc-due="btcDue" :is-settled="isSettled" :is-processing="isProcessing" :payment-method-id="pmId" :crypto-code="srvModel.cryptoCode"></checkout-cheating>
|
|
}
|
|
<footer class="store-footer">
|
|
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
|
{{$t("powered_by")}} <partial name="_StoreFooterLogo" />
|
|
</a>
|
|
<select asp-for="DefaultLang" asp-items="@LangService.GetLanguageSelectListItems()" class="form-select" v-on:change="changeLanguage"></select>
|
|
</footer>
|
|
</div>
|
|
<noscript>
|
|
<div class="p-5 text-center">
|
|
<h2>Javascript is currently disabled in your browser.</h2>
|
|
<h5>Please enable Javascript and refresh this page for the best experience.</h5>
|
|
<p>
|
|
Alternatively, click below to continue to our
|
|
<a asp-action="CheckoutNoScript" asp-route-invoiceId="@Model.InvoiceId">HTML-only invoice</a>.
|
|
</p>
|
|
</div>
|
|
</noscript>
|
|
<script type="text/x-template" id="payment-details">
|
|
<dl>
|
|
<div v-if="orderAmount > 0" id="PaymentDetails-TotalPrice" key="TotalPrice">
|
|
<dt v-t="'total_price'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.orderAmount)" data-clipboard-hover="start">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</dd>
|
|
</div>
|
|
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat" id="PaymentDetails-TotalFiat" key="TotalFiat">
|
|
<dt v-t="'total_fiat'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.orderAmountFiat)" data-clipboard-hover="start">{{srvModel.orderAmountFiat}}</dd>
|
|
</div>
|
|
<div v-if="srvModel.rate && srvModel.cryptoCode" id="PaymentDetails-ExchangeRate" key="ExchangeRate">
|
|
<dt v-t="'exchange_rate'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.rate)" data-clipboard-hover="start">
|
|
<template v-if="srvModel.cryptoCode === 'sats'">1 sat = {{ srvModel.rate }}</template>
|
|
<template v-else>1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}</template>
|
|
</dd>
|
|
</div>
|
|
<div v-if="srvModel.networkFee" id="PaymentDetails-NetworkCost" key="NetworkCost">
|
|
<dt v-t="'network_cost'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.networkFee)" data-clipboard-hover="start">
|
|
<div v-if="srvModel.txCountForFee > 0" v-t="{ path: 'tx_count', args: { count: srvModel.txCount } }"></div>
|
|
<div v-text="`${srvModel.networkFee} ${srvModel.cryptoCode}`"></div>
|
|
</dd>
|
|
</div>
|
|
<div v-if="btcPaid > 0" id="PaymentDetails-AmountPaid" key="AmountPaid">
|
|
<dt v-t="'amount_paid'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.btcPaid)" data-clipboard-hover="start" v-text="`${srvModel.btcPaid} ${srvModel.cryptoCode}`"></dd>
|
|
</div>
|
|
<div v-if="btcDue > 0" id="PaymentDetails-AmountDue" key="AmountDue">
|
|
<dt v-t="'amount_due'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.btcDue)" data-clipboard-hover="start" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`"></dd>
|
|
</div>
|
|
<div v-if="showRecommendedFee" id="PaymentDetails-RecommendedFee" key="RecommendedFee">
|
|
<dt v-t="'recommended_fee'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.feeRate)" data-clipboard-hover="start" v-t="{ path: 'fee_rate', args: { feeRate: srvModel.feeRate } }"></dd>
|
|
</div>
|
|
</dl>
|
|
</script>
|
|
<script>
|
|
const i18nUrl = @Safe.Json($"{Model.RootPath}misc/translations/checkout/{{{{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>
|
|
<script src="~/vendor/i18next/i18nextHttpBackend.min.js" asp-append-version="true"></script>
|
|
<script src="~/vendor/i18next/vue-i18next.js" asp-append-version="true"></script>
|
|
<script src="~/js/copy-to-clipboard.js" asp-append-version="true"></script>
|
|
<script src="~/js/vue-utils.js" asp-append-version="true"></script>
|
|
<script src="~/main/utils.js" asp-append-version="true"></script>
|
|
<script src="~/checkout/checkout.js" asp-append-version="true"></script>
|
|
@if (Env.CheatMode)
|
|
{
|
|
<partial name="Checkout-Cheating" model="@Model" />
|
|
}
|
|
@foreach (var extensionPartial in Model.ExtensionPartials)
|
|
{
|
|
<partial name="@extensionPartial" model="@Model" />
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-end", model = Model })
|
|
</body>
|
|
</html>
|