Checkout v2: Payment processing state (#4778)

This commit is contained in:
d11n 2023-03-27 12:12:11 +02:00 committed by GitHub
parent de9ac9fd43
commit 45141d1391
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 187 additions and 72 deletions

View file

@ -140,7 +140,7 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("Please send", paymentInfo.Text); Assert.DoesNotContain("Please send", paymentInfo.Text);
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
var expiredSection = s.Driver.FindElement(By.Id("expired")); var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
Assert.True(expiredSection.Displayed); Assert.True(expiredSection.Displayed);
Assert.Contains("Invoice Expired", expiredSection.Text); Assert.Contains("Invoice Expired", expiredSection.Text);
}); });
@ -181,12 +181,27 @@ namespace BTCPayServer.Tests
{ {
Assert.Contains("Created transaction", Assert.Contains("Created transaction",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text); s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
s.Server.ExplorerNode.Generate(1); s.Server.ExplorerNode.Generate(2);
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo")); paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text); Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text); Assert.Contains("Please send", paymentInfo.Text);
}); });
// Pay full amount
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
s.Driver.FindElement(By.Id("FakePay")).Click();
// Processing
TestUtils.Eventually(() =>
{
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Sent", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("confetti")));
});
// Mine // Mine
s.Driver.FindElement(By.Id("Mine")).Click(); s.Driver.FindElement(By.Id("Mine")).Click();
TestUtils.Eventually(() => TestUtils.Eventually(() =>
@ -195,16 +210,12 @@ namespace BTCPayServer.Tests
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text); s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
}); });
// Pay full amount // Settled
var amountDue = s.Driver.FindElement(By.Id("AmountDue")).GetAttribute("data-amount-due");
s.Driver.FindElement(By.Id("FakePayAmount")).FillIn(amountDue);
s.Driver.FindElement(By.Id("FakePay")).Click();
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
s.Server.ExplorerNode.Generate(1); var settledSection = s.Driver.WaitForElement(By.Id("settled"));
var paidSection = s.Driver.WaitForElement(By.Id("paid")); Assert.True(settledSection.Displayed);
Assert.True(paidSection.Displayed); Assert.Contains("Invoice Paid", settledSection.Text);
Assert.Contains("Invoice Paid", paidSection.Text);
}); });
s.Driver.FindElement(By.Id("confetti")); s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("ReceiptLink")); s.Driver.FindElement(By.Id("ReceiptLink"));

View file

@ -80,15 +80,13 @@ namespace BTCPayServer.Controllers
} }
return UnprocessableEntity(new return UnprocessableEntity(new
{ {
ErrorMessage = response.ErrorDetail, ErrorMessage = response.ErrorDetail
AmountRemaining = invoice.Price
}); });
default: default:
return UnprocessableEntity(new return UnprocessableEntity(new
{ {
ErrorMessage = $"Payment method {paymentMethodId} is not supported", ErrorMessage = $"Payment method {paymentMethodId} is not supported"
AmountRemaining = invoice.Price
}); });
} }
@ -97,8 +95,7 @@ namespace BTCPayServer.Controllers
{ {
return BadRequest(new return BadRequest(new
{ {
ErrorMessage = e.Message, ErrorMessage = e.Message
AmountRemaining = invoice.Price
}); });
} }
} }

View file

@ -829,6 +829,15 @@ namespace BTCPayServer.Controllers
NetworkFeeMode.Never => 0, NetworkFeeMode.Never => 0,
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}, },
RequiredConfirmations = invoice.SpeedPolicy switch
{
SpeedPolicy.HighSpeed => 0,
SpeedPolicy.MediumSpeed => 1,
SpeedPolicy.LowMediumSpeed => 2,
SpeedPolicy.LowSpeed => 6,
_ => null
},
ReceivedConfirmations = invoice.GetAllBitcoinPaymentData(false).FirstOrDefault()?.ConfirmationCount,
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString, Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete

View file

@ -77,5 +77,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string ReceiptLink { get; set; } public string ReceiptLink { get; set; }
public bool AltcoinsBuild { get; set; } public bool AltcoinsBuild { get; set; }
public CheckoutType CheckoutType { get; set; } public CheckoutType CheckoutType { get; set; }
public int? RequiredConfirmations { get; set; }
public long? ReceivedConfirmations { get; set; }
} }
} }

View file

@ -1,22 +1,25 @@
@model PaymentModel @model PaymentModel
<style>
#checkout-cheating form + form { margin-top: var(--btcpay-space-l); }
</style>
<main id="checkout-cheating" class="shadow-lg" v-cloak v-if="display"> <main id="checkout-cheating" class="shadow-lg" v-cloak v-if="display">
<section> <section>
<p id="CheatSuccessMessage" class="alert alert-success text-break" v-if="successMessage" v-text="successMessage"></p> <p id="CheatSuccessMessage" class="alert alert-success text-break" v-if="successMessage" v-text="successMessage"></p>
<p id="CheatErrorMessage" class="alert alert-danger text-break" v-if="errorMessage" v-text="errorMessage"></p> <p id="CheatErrorMessage" class="alert alert-danger text-break" v-if="errorMessage" v-text="errorMessage"></p>
<form id="test-payment" :action="`/i/${invoiceId}/test-payment`" method="post" v-on:submit.prevent="handleFormSubmit($event, 'paying')" v-if="displayPayment"> <form id="test-payment" :action="`/i/${invoiceId}/test-payment`" method="post" v-on:submit.prevent="handleFormSubmit($event, 'paying')" v-if="displayPayment">
<input name="CryptoCode" type="hidden" value="@Model.CryptoCode"> <input name="CryptoCode" type="hidden" :value="cryptoCode">
<input name="PaymentMethodId" type="hidden" :value="paymentMethodId"> <input name="PaymentMethodId" type="hidden" :value="paymentMethodId">
<label for="FakePayAmount" class="control-label form-label">Fake a @Model.CryptoCode payment for testing</label> <label for="FakePayAmount" class="control-label form-label">Fake a {{cryptoCode}} payment for testing</label>
<div class="d-flex gap-2 mb-2"> <div class="d-flex gap-2 mb-2">
<div class="input-group"> <div class="input-group">
<input id="FakePayAmount" name="Amount" type="number" step="0.00000001" min="0" class="form-control" placeholder="Amount" v-model="amountRemaining" :disabled="paying || paymentMethodId === 'BTC_LightningLike'"/> <input id="FakePayAmount" name="Amount" type="number" :step="isSats ? '1' : '0.00000001'" min="0" class="form-control" placeholder="Amount" v-model="amountRemaining" :disabled="paying || paymentMethodId === 'BTC_LightningLike'"/>
<div id="test-payment-crypto-code" class="input-group-addon input-group-text">@Model.CryptoCode</div> <div id="test-payment-crypto-code" class="input-group-addon input-group-text" v-text="cryptoCode"></div>
</div> </div>
<button class="btn btn-secondary flex-shrink-0 px-3 w-100px" type="submit" :disabled="paying" id="FakePay">Pay</button> <button class="btn btn-secondary flex-shrink-0 px-3 w-100px" type="submit" :disabled="paying" id="FakePay">Pay</button>
</div> </div>
</form> </form>
<form id="mine-block" :action="`/i/${invoiceId}/mine-blocks`" method="post" class="mt-4" v-on:submit.prevent="handleFormSubmit($event, 'mining')" v-if="displayMine"> <form id="mine-block" :action="`/i/${invoiceId}/mine-blocks`" method="post" v-on:submit.prevent="handleFormSubmit($event, 'mining')" v-if="displayMine">
<label for="BlockCount" class="control-label form-label">Mine to test processing and settlement</label> <label for="BlockCount" class="control-label form-label">Mine to test processing and settlement</label>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<div class="input-group"> <div class="input-group">
@ -26,7 +29,7 @@
<button class="btn btn-secondary flex-shrink-0 px-3 w-100px" type="submit" :disabled="mining" id="Mine">Mine</button> <button class="btn btn-secondary flex-shrink-0 px-3 w-100px" type="submit" :disabled="mining" id="Mine">Mine</button>
</div> </div>
</form> </form>
<form id="expire-invoice" :action="`/i/${invoiceId}/expire`" method="post" class="mt-4" v-on:submit.prevent="handleFormSubmit($event, 'expiring')" v-if="displayExpire"> <form id="expire-invoice" :action="`/i/${invoiceId}/expire`" method="post" v-on:submit.prevent="handleFormSubmit($event, 'expiring')" v-if="displayExpire">
<label for="ExpirySeconds" class="control-label form-label">Expire invoice in …</label> <label for="ExpirySeconds" class="control-label form-label">Expire invoice in …</label>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<div class="input-group"> <div class="input-group">
@ -49,27 +52,32 @@
paying: false, paying: false,
mining: false, mining: false,
expiring: false, expiring: false,
amountRemaining: parseFloat(this.btcDue) amountRemaining: this.btcDue
} }
}, },
props: { props: {
invoiceId: String, invoiceId: String,
paymentMethodId: String, paymentMethodId: String,
cryptoCode: String,
btcDue: Number, btcDue: Number,
isPaid: Boolean isProcessing: Boolean,
isSettled: Boolean
}, },
computed: { computed: {
display() { display() {
return this.successMessage || this.errorMessage || this.displayPayment || this.displayMine || this.displayExpire; return this.successMessage || this.errorMessage || this.displayPayment || this.displayMine || this.displayExpire;
}, },
displayPayment () { displayPayment () {
return !this.isPaid; return !this.isSettled && !this.isProcessing;
}, },
displayExpire () { displayExpire () {
return !this.isPaid; return !this.isSettled && !this.isProcessing;
}, },
displayMine () { displayMine () {
return this.paymentMethodId === 'BTC'; return this.paymentMethodId === 'BTC';
},
isSats () {
return this.cryptoCode === 'sats';
} }
}, },
methods: { methods: {

View file

@ -76,7 +76,14 @@
<vc:icon symbol="caret-down" /> <vc:icon symbol="caret-down" />
</button> </button>
<div id="PaymentDetails" class="payment-details" v-collapsible="displayPaymentDetails"> <div id="PaymentDetails" class="payment-details" v-collapsible="displayPaymentDetails">
<payment-details :srv-model="srvModel" :is-active="isActive" class="pb-4"></payment-details> <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> </div>
@if (displayedPaymentMethods.Count > 1 || hasPaymentPlugins) @if (displayedPaymentMethods.Count > 1 || hasPaymentPlugins)
{ {
@ -99,11 +106,45 @@
<component v-if="paymentMethodComponent" :is="paymentMethodComponent" :model="srvModel" /> <component v-if="paymentMethodComponent" :is="paymentMethodComponent" :model="srvModel" />
</section> </section>
<section id="result" v-else> <section id="result" v-else>
<div id="paid" v-if="isPaid"> <div v-if="isProcessing" id="processing" key="processing">
<div class="top">
<span class="icn">
<vc:icon symbol="payment-sent" />
</span>
<h4 v-t="'payment_sent'"></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-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>
<p class="text-center mt-3" v-t="'payment_sent_body'"></p>
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations != null" v-t="{ path: 'payment_sent_confirmations', args: { cryptoCode: realCryptoCode, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
</div>
</div>
<div v-if="isSettled" id="settled" key="settled">
<div class="top"> <div class="top">
<span class="icn"> <span class="icn">
<div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div> <div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div>
<vc:icon symbol="payment-complete"/> <vc:icon symbol="payment-complete" />
</span> </span>
<h4 v-t="'invoice_paid'"></h4> <h4 v-t="'invoice_paid'"></h4>
<div id="PaymentDetails" class="payment-details"> <div id="PaymentDetails" class="payment-details">
@ -117,16 +158,23 @@
<dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd> <dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd>
</div> </div>
</dl> </dl>
<payment-details :srv-model="srvModel" :is-active="isActive" class="mb-5"></payment-details> <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> </div>
<div class="buttons"> <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="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> <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> <button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
</div> </div>
</div> </div>
<div id="expired" v-if="isUnpayable"> <div v-if="isInvalid" id="unpaid" key="unpaid">
<div class="top"> <div class="top">
<span class="icn"> <span class="icn">
<vc:icon symbol="invoice-expired" /> <vc:icon symbol="invoice-expired" />
@ -143,7 +191,14 @@
<dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd> <dd v-text="srvModel.orderId" :data-clipboard="srvModel.orderId" data-clipboard-hover="start"></dd>
</div> </div>
</dl> </dl>
<payment-details :srv-model="srvModel" :is-active="isActive" v-collapsible="displayPaymentDetails"></payment-details> <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> </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"> <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> <span class="fw-semibold" v-t="'view_details'"></span>
@ -160,7 +215,7 @@
</main> </main>
@if (Env.CheatMode) @if (Env.CheatMode)
{ {
<checkout-cheating invoice-id="@Model.InvoiceId" :btc-due="srvModel.btcDue" :is-paid="isPaid" :payment-method-id="pmId"></checkout-cheating> <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"> <footer class="store-footer">
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener"> <a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">

View file

@ -148,10 +148,11 @@ section dl > div dd {
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
} }
#result #paid .top .icn .icon { #result #settled .top .icn .icon,
#result #processing .top .icn .icon {
color: var(--btcpay-primary); color: var(--btcpay-primary);
} }
#result #expired .top .icn .icon { #result #unpaid .top .icn .icon {
color: var(--btcpay-body-text-muted); color: var(--btcpay-body-text-muted);
} }
#DefaultLang { #DefaultLang {

View file

@ -36,8 +36,11 @@ Vue.directive('collapsible', {
} }
}); });
const STATUS_PAID = ['complete', 'confirmed', 'paid']; // These are the legacy states, see InvoiceEntity
const STATUS_UNPAYABLE = ['expired', 'invalid']; const STATUS_PAYABLE = ['new'];
const STATUS_PAID = ['paid'];
const STATUS_SETTLED = ['complete', 'confirmed'];
const STATUS_INVALID = ['expired', 'invalid'];
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
function computeStartingLanguage() { function computeStartingLanguage() {
@ -82,21 +85,11 @@ const PaymentDetails = {
template: '#payment-details', template: '#payment-details',
props: { props: {
srvModel: Object, srvModel: Object,
isActive: Boolean isActive: Boolean,
}, showRecommendedFee: Boolean,
computed: { orderAmount: Number,
orderAmount () { btcPaid: Number,
return parseFloat(this.srvModel.orderAmount); btcDue: Number
},
btcDue () {
return parseFloat(this.srvModel.btcDue);
},
btcPaid () {
return parseFloat(this.srvModel.btcPaid);
},
showRecommendedFee () {
return this.isActive && this.srvModel.showRecommendedFee && this.srvModel.feeRate;
},
} }
} }
@ -118,18 +111,22 @@ function initApp() {
emailAddressInputInvalid: false, emailAddressInputInvalid: false,
paymentMethodId: null, paymentMethodId: null,
endData: null, endData: null,
isModal: srvModel.isModal isModal: srvModel.isModal,
pollTimeoutID: null
} }
}, },
computed: { computed: {
isUnpayable () { isInvalid () {
return STATUS_UNPAYABLE.includes(this.srvModel.status); return STATUS_INVALID.includes(this.srvModel.status);
}, },
isPaid () { isSettled () {
return STATUS_SETTLED.includes(this.srvModel.status);
},
isProcessing () {
return STATUS_PAID.includes(this.srvModel.status); return STATUS_PAID.includes(this.srvModel.status);
}, },
isActive () { isActive () {
return !this.isUnpayable && !this.isPaid; return STATUS_PAYABLE.includes(this.srvModel.status);
}, },
showInfo () { showInfo () {
return this.showTimer || this.showPaymentDueInfo; return this.showTimer || this.showPaymentDueInfo;
@ -141,16 +138,16 @@ function initApp() {
return this.btcPaid > 0 && this.btcDue > 0; return this.btcPaid > 0 && this.btcDue > 0;
}, },
showRecommendedFee () { showRecommendedFee () {
return this.isActive() && this.srvModel.showRecommendedFee && this.srvModel.feeRate; return this.isActive && this.srvModel.showRecommendedFee && this.srvModel.feeRate;
}, },
orderAmount () { orderAmount () {
return parseFloat(this.srvModel.orderAmount); return this.asNumber(this.srvModel.orderAmount);
}, },
btcDue () { btcDue () {
return parseFloat(this.srvModel.btcDue); return this.asNumber(this.srvModel.btcDue);
}, },
btcPaid () { btcPaid () {
return parseFloat(this.srvModel.btcPaid); return this.asNumber(this.srvModel.btcPaid);
}, },
pmId () { pmId () {
return this.paymentMethodId || this.srvModel.paymentMethodId; return this.paymentMethodId || this.srvModel.paymentMethodId;
@ -181,13 +178,26 @@ function initApp() {
}, },
isPluginPaymentMethod () { isPluginPaymentMethod () {
return !this.paymentMethodIds.includes(this.pmId); return !this.paymentMethodIds.includes(this.pmId);
},
realCryptoCode () {
return this.srvModel.cryptoCode.toLowerCase() === 'sats' ? 'BTC' : this.srvModel.cryptoCode;
} }
}, },
watch: { watch: {
isPaid: function (newValue, oldValue) { isProcessing: function (newValue, oldValue) {
if (newValue === true && oldValue === false) {
// poll from here on
this.listenForConfirmations();
}
},
isSettled: function (newValue, oldValue) {
if (newValue === true && oldValue === false) { if (newValue === true && oldValue === false) {
const duration = 5000; const duration = 5000;
const self = this; const self = this;
// stop polling
if (this.pollTimeoutID) {
clearTimeout(this.pollTimeoutID);
}
// celebration! // celebration!
Vue.nextTick(function () { Vue.nextTick(function () {
self.celebratePayment(duration); self.celebratePayment(duration);
@ -208,9 +218,12 @@ function initApp() {
mounted () { mounted () {
this.updateData(this.srvModel); this.updateData(this.srvModel);
this.updateTimer(); this.updateTimer();
if (this.isActive) { if (this.isActive || this.isProcessing) {
this.listenIn(); this.listenIn();
} }
if (this.isProcessing) {
this.listenForConfirmations();
}
updateLanguageSelect(); updateLanguageSelect();
window.parent.postMessage('loaded', '*'); window.parent.postMessage('loaded', '*');
}, },
@ -224,6 +237,9 @@ function initApp() {
changeLanguage (e) { changeLanguage (e) {
updateLanguage(e.target.value); updateLanguage(e.target.value);
}, },
asNumber (val) {
return parseFloat(val.replace(/\s/g, '')); // e.g. sats are formatted with spaces: 1 000 000
},
padTime (val) { padTime (val) {
return val.toString().padStart(2, '0'); return val.toString().padStart(2, '0');
}, },
@ -257,13 +273,26 @@ function initApp() {
} }
} }
// fallback in case there is no websocket support // fallback in case there is no websocket support
(function watcher() { if (!socket || socket.readyState !== 1) {
setTimeout(async function () { this.pollUpdates(2000, socket)
if (socket === null || socket.readyState !== 1) { }
},
listenForConfirmations () {
this.pollUpdates(30000);
},
pollUpdates (interval, socket) {
const self = this;
const updateFn = this.fetchData;
if (self.pollTimeoutID) {
clearTimeout(self.pollTimeoutID);
}
(function pollFn() {
self.pollTimeoutID = setTimeout(async function () {
if (!socket || socket.readyState !== 1) {
await updateFn(); await updateFn();
pollFn();
} }
watcher(); }, interval);
}, 2000);
})(); })();
}, },
async fetchData () { async fetchData () {

View file

@ -26,6 +26,9 @@
"address": "Address", "address": "Address",
"lightning": "Lightning", "lightning": "Lightning",
"payment_link": "Payment Link", "payment_link": "Payment Link",
"payment_sent": "Payment Sent",
"payment_sent_body": "Your payment has been received and is now processing.",
"payment_sent_confirmations": "Currently it has {{receivedConfirmations}} confirmations. Once it receives {{requiredConfirmations}} confirmations on the {{cryptoCode}} blockchain, the status will be updated to settled.",
"invoice_paid": "Invoice Paid", "invoice_paid": "Invoice Paid",
"invoice_expired": "Invoice Expired", "invoice_expired": "Invoice Expired",
"invoice_expired_body": "An invoice is only valid for {{minutes}} minutes.\n\nReturn to {{storeName}} if you would like to resubmit a payment.", "invoice_expired_body": "An invoice is only valid for {{minutes}} minutes.\n\nReturn to {{storeName}} if you would like to resubmit a payment.",
@ -34,5 +37,5 @@
"copy": "Copy", "copy": "Copy",
"copy_confirm": "Copied", "copy_confirm": "Copied",
"powered_by": "Powered by", "powered_by": "Powered by",
"conversion_body": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain." "conversion_body": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on the {{cryptoCode}} blockchain."
} }