btcpayserver/BTCPayServer/Views/UIInvoice/Checkout.cshtml
2024-10-25 13:11:26 +02:00

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>