btcpayserver/BTCPayServer/Views/UIInvoice/CheckoutV2.cshtml
d11n d73d0f178f
Checkout: Allow NFC/LNURL-W whenever LNURL is available (#4671)
* Checkout: Allow NFC/LNURL-W whenever LNURL is available

With what we have in master right now, we display NFC only for top-up invoices. With these changes, we display NFC in all cases, where LNURL is available.

Note that this hides LNURL from the list of selectable payment methods, it's only available to use the NFC — and explicitely selectable only for the edge case of top-up invoice + non-unified QR (as before).

Rationale: Now that we got NFC tightly integrated, it doesn't make sense to support the NFC experience only for top-up invoices. With this we bring back LNURL for regular invoices as well, but don't make it selectable and use it only for the NFC functionality.

* Fix LNURL condition

* Improve and test NFC/LNURL display condition

Restores what was fixed in #4660.

* Fix and test Lightning-only case

* Add cache busting for locales
2023-02-22 15:53:14 +09:00

245 lines
14 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 v-waitForT>
<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">
<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="text-center" v-t="'any_amount'"></h2>
}
else
{
<h2 id="AmountDue" class="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-t="{ path: 'return_to_store', args: { 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-t="{ path: 'return_to_store', args: { 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>
<dl id="payment-details" v-cloak>
<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>
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>