mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 14:40:36 +01:00
* Prevent duplicate titles on invoice view * Fix text display of escaped values Fixes #4696. * Fix payment details re-rendering Closes #4683. Closes #4684. * Cleanup
250 lines
15 KiB
Text
250 lines
15 KiB
Text
@using BTCPayServer.Services
|
|
@using BTCPayServer.Abstractions.Contracts
|
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@inject LanguageService LangService
|
|
@inject BTCPayServerEnvironment Env
|
|
@inject IEnumerable<IUIExtension> UiExtensions
|
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
|
@model PaymentModel
|
|
@{
|
|
Layout = null;
|
|
ViewData["Title"] = Model.HtmlTitle;
|
|
|
|
var hasPaymentPlugins = UiExtensions.Any(extension => extension.Location == "checkout-payment-method");
|
|
// Show LNURL as selectable payment method only for top up invoices + non-BIP21 case
|
|
var displayedPaymentMethods = Model.IsUnsetTopUp && !Model.OnChainWithLnInvoiceFallback
|
|
? Model.AvailableCryptos
|
|
: Model.AvailableCryptos.Where(c => c.PaymentMethodId != "BTC_LNURLPAY").ToList();
|
|
}
|
|
@functions {
|
|
private string PaymentMethodName(PaymentModel.AvailableCrypto pm)
|
|
{
|
|
return Model.AltcoinsBuild
|
|
? pm.PaymentMethodName
|
|
: pm.PaymentMethodName.Replace("Bitcoin (", "").Replace(")", "").Replace("Lightning ", "");
|
|
}
|
|
|
|
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-v2/checkout.css" asp-append-version="true" rel="stylesheet" />
|
|
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, "", "")" />
|
|
</head>
|
|
<body class="min-vh-100">
|
|
<div id="Checkout-v2" class="public-page-wrap" v-cloak>
|
|
<partial name="_StoreHeader" model="(Model.StoreName, Model.LogoFileId)" />
|
|
<main class="shadow-lg">
|
|
<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">
|
|
@if (!string.IsNullOrEmpty(Model.ItemDesc) && Model.ItemDesc != Model.StoreName)
|
|
{
|
|
<h5 class="text-center mt-1 mb-3 fw-semibold" v-if="srvModel.itemDesc" v-text="srvModel.itemDesc">@Model.ItemDesc</h5>
|
|
}
|
|
@if (Model.IsUnsetTopUp)
|
|
{
|
|
<h2 id="AmountDue" class="mt-1 text-center" v-t="'any_amount'"></h2>
|
|
}
|
|
else
|
|
{
|
|
<h2 id="AmountDue" class="mt-1 text-center" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`" :data-clipboard="srvModel.btcDue" :data-clipboard-confirm="$t('copy_confirm')" :data-amount-due="srvModel.btcDue">@Model.BtcDue @Model.CryptoCode</h2>
|
|
}
|
|
<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" class="pb-4"></payment-details>
|
|
</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))">
|
|
@PaymentMethodName(crypto)
|
|
</a>
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment-method", model = Model })
|
|
</div>
|
|
</div>
|
|
}
|
|
<component v-if="paymentMethodComponent" :is="paymentMethodComponent" :model="srvModel" />
|
|
</section>
|
|
<section id="result" v-else>
|
|
<div id="paid" v-if="isPaid">
|
|
<div class="top">
|
|
<span class="icn">
|
|
<vc:icon symbol="payment-complete"/>
|
|
</span>
|
|
<h4 v-t="'invoice_paid'"></h4>
|
|
<dl class="mb-0">
|
|
<div>
|
|
<dt v-t="'invoice_id'"></dt>
|
|
<dd v-text="srvModel.invoiceId"></dd>
|
|
</div>
|
|
<div v-if="srvModel.orderId">
|
|
<dt v-t="'order_id'"></dt>
|
|
<dd v-text="srvModel.orderId"></dd>
|
|
</div>
|
|
</dl>
|
|
<payment-details :srv-model="srvModel" :is-active="isActive" class="mb-5"></payment-details>
|
|
</div>
|
|
<div class="buttons">
|
|
<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 id="expired" v-if="isUnpayable">
|
|
<div class="top">
|
|
<span class="icn">
|
|
<vc:icon symbol="invoice-expired"/>
|
|
</span>
|
|
<h4 v-t="'invoice_expired'"></h4>
|
|
<dl class="mb-0">
|
|
<div>
|
|
<dt v-t="'invoice_id'"></dt>
|
|
<dd v-text="srvModel.invoiceId"></dd>
|
|
</div>
|
|
<div v-if="srvModel.orderId">
|
|
<dt v-t="'order_id'"></dt>
|
|
<dd v-text="srvModel.orderId"></dd>
|
|
</div>
|
|
</dl>
|
|
<div id="PaymentDetails" class="payment-details" v-collapsible="displayPaymentDetails">
|
|
<payment-details :srv-model="srvModel" :is-active="isActive"></payment-details>
|
|
</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('invoice_expired_body', { storeName: srvModel.storeName, minutes: @Model.MaxTimeMinutes }))"></p>
|
|
</div>
|
|
<div class="buttons">
|
|
<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="srvModel.btcDue" :is-paid="isPaid" :payment-method-id="pmId"></checkout-cheating>
|
|
}
|
|
<footer class="store-footer">
|
|
<a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
|
{{$t("powered_by")}} <partial name="_StoreFooterLogo" />
|
|
</a>
|
|
@* TODO: Re-add this once checkout v2 has been translated
|
|
<select asp-for="DefaultLang" asp-items="@LangService.GetLanguageSelectListItems()" class="form-select w-auto" 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">
|
|
<dt v-t="'total_price'"></dt>
|
|
<dd :data-clipboard="srvModel.orderAmount" :data-clipboard-confirm="$t('copy_confirm')">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</dd>
|
|
</div>
|
|
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat">
|
|
<dt v-t="'total_fiat'"></dt>
|
|
<dd :data-clipboard="srvModel.orderAmountFiat" :data-clipboard-confirm="$t('copy_confirm')">{{srvModel.orderAmountFiat}}</dd>
|
|
</div>
|
|
<div v-if="srvModel.rate && srvModel.cryptoCode">
|
|
<dt v-t="'exchange_rate'"></dt>
|
|
<dd :data-clipboard="srvModel.rate" :data-clipboard-confirm="$t('copy_confirm')">
|
|
<template v-if="srvModel.cryptoCodeSrv === 'Sats'">1 Sat = {{ srvModel.rate }}</template>
|
|
<template v-else>1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}</template>
|
|
</dd>
|
|
</div>
|
|
<div v-if="srvModel.networkFee">
|
|
<dt v-t="'network_cost'"></dt>
|
|
<dd :data-clipboard="srvModel.networkFee" :data-clipboard-confirm="$t('copy_confirm')">
|
|
<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">
|
|
<dt v-t="'amount_paid'"></dt>
|
|
<dd :data-clipboard="srvModel.btcPaid" :data-clipboard-confirm="$t('copy_confirm')" v-text="`${srvModel.btcPaid} ${srvModel.cryptoCode}`"></dd>
|
|
</div>
|
|
<div v-if="btcDue > 0">
|
|
<dt v-t="'amount_due'"></dt>
|
|
<dd :data-clipboard="srvModel.btcDue" :data-clipboard-confirm="$t('copy_confirm')" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`"></dd>
|
|
</div>
|
|
<div v-if="showRecommendedFee">
|
|
<dt v-t="'recommended_fee'"></dt>
|
|
<dd :data-clipboard="srvModel.feeRate" :data-clipboard-confirm="$t('copy_confirm')" v-t="{ path: 'fee_rate', args: { feeRate: srvModel.feeRate } }"></dd>
|
|
</div>
|
|
</dl>
|
|
</script>
|
|
<script>
|
|
const i18nUrl = @Safe.Json($"{Model.RootPath}locales/checkout/{{{{lng}}}}.json?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 = ['en']; // @Safe.Json(LangService.GetLanguages().Select(language => language.Code));
|
|
const initialSrvModel = @Safe.Json(Model);
|
|
const qrOptions = { margin: 0, type: 'svg', color: { dark: '#000', light: '#fff' } };
|
|
</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="~/main/utils.js" asp-append-version="true"></script>
|
|
<script src="~/checkout-v2/checkout.js" asp-append-version="true"></script>
|
|
@if (Env.CheatMode)
|
|
{
|
|
<partial name="Checkout-Cheating" model="@Model" />
|
|
}
|
|
@foreach (var paymentMethodHandler in PaymentMethodHandlerDictionary
|
|
.Select(handler => handler.GetCheckoutUISettings())
|
|
.Where(settings => settings != null)
|
|
.DistinctBy(pm => pm.ExtensionPartial))
|
|
{
|
|
<partial name="@paymentMethodHandler.ExtensionPartial-v2" model="@Model"/>
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment", model = Model })
|
|
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-v2-end", model = Model })
|
|
</body>
|
|
</html>
|