mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
321 lines
20 KiB
Plaintext
321 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 CheckoutModel
|
|
@{
|
|
Layout = null;
|
|
ViewData["Title"] = Model.HtmlTitle;
|
|
ViewData["StoreBranding"] = Model.StoreBranding;
|
|
Csp.UnsafeEval();
|
|
var hasPaymentPlugins = UiExtensions.Any(extension => extension.Location == "checkout-payment-method");
|
|
var checkoutLink = Url.Action("Checkout", new { invoiceId = Model.InvoiceId });
|
|
}
|
|
@functions {
|
|
private string ToJsValue(object value)
|
|
{
|
|
return Safe.Json(value?.ToString()).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-break 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.due} ${srvModel.paymentMethodCurrency}`" :data-clipboard="asNumber(srvModel.due)" data-clipboard-hover :data-amount-due="srvModel.due">@Model.Due @Model.PaymentMethodCurrency</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.due} ${srvModel.paymentMethodCurrency}` }))"></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"
|
|
:paid="paid"
|
|
:due="due"
|
|
:show-recommended-fee="showRecommendedFee"
|
|
class="pb-4" />
|
|
</div>
|
|
<div v-if="displayedPaymentMethods.length > 1 || @Safe.Json(hasPaymentPlugins)" 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">
|
|
<a
|
|
v-for="crypto in displayedPaymentMethods"
|
|
:href="@ToJsValue(checkoutLink) + '/' + crypto.paymentMethodId"
|
|
class="btcpay-pill m-0 payment-method"
|
|
:class="{ active: srvModel.paymentMethodId === crypto.paymentMethodId }"
|
|
v-on:click.prevent="changePaymentMethod(crypto.paymentMethodId)"
|
|
v-text="crypto.paymentMethodName">
|
|
</a>
|
|
</div>
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment-method", model = Model })
|
|
</div>
|
|
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
|
|
:model="srvModel"
|
|
:nfc-scanning="nfc.scanning"
|
|
:nfc-supported="nfc.supported"
|
|
:nfc-error-message="nfc.errorMessage"
|
|
:nfc-warning-message="nfc.warningMessage"
|
|
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: realPaymentMethodCurrency, 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"
|
|
:paid="paid"
|
|
:due="due"
|
|
: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"
|
|
:paid="paid"
|
|
:due="due"
|
|
: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"
|
|
:paid="paid"
|
|
:due="due"
|
|
: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" :due="due" :is-settled="isSettled" :is-processing="isProcessing" :payment-method-id="pmId" :crypto-code="srvModel.paymentMethodCurrency"></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.paymentMethodCurrency }}</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.paymentMethodCurrency" 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.paymentMethodCurrency === 'sats'">1 sat = {{ srvModel.rate }}</template>
|
|
<template v-else>1 {{ srvModel.paymentMethodCurrency }} = {{ 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.paymentMethodCurrency}`"></div>
|
|
</dd>
|
|
</div>
|
|
<div v-if="paid > 0" id="PaymentDetails-AmountPaid" key="AmountPaid">
|
|
<dt v-t="'amount_paid'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.paid)" data-clipboard-hover="start" v-text="`${srvModel.paid} ${srvModel.paymentMethodCurrency}`"></dd>
|
|
</div>
|
|
<div v-if="due > 0" id="PaymentDetails-AmountDue" key="AmountDue">
|
|
<dt v-t="'amount_due'"></dt>
|
|
<dd :data-clipboard="asNumber(srvModel.due)" data-clipboard-hover="start" v-text="`${srvModel.due} ${srvModel.paymentMethodCurrency}`"></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" />
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-end", model = Model })
|
|
</body>
|
|
</html>
|