USD and XMR payments now working

This commit is contained in:
Keith Irwin 2023-04-03 22:20:59 -06:00
parent 2f5d7eac59
commit b46c202a13
Signed by: ki9
GPG Key ID: DF773B3F4A88DA86
5 changed files with 89 additions and 50 deletions

View File

@ -56,6 +56,7 @@ const emptyCart = () => {
localStorage.removeItem(i)
}); recountCart()
}
let xmr_price = 160
//let socket
@ -65,8 +66,8 @@ class MoneroTransaction { constructor(data) {
self.confirmations = ko.observable(0)
self.double_spend_seen = ko.observable(false)
self.fee = data.fee/1000000000000
self.fee_usd = self.fee * self.xmr_price()
self.fee_usd_pretty = `$${self.fee_usd}`
self.fee_usd = self.fee * xmr_price
self.fee_usd_pretty = `$${self.fee_usd.toFixed(4)}`
self.height = data.height
self.datetime = new Date(data.timestamp)
self.date = self.datetime.toLocaleDateString()
@ -80,6 +81,9 @@ class Checkout { constructor(data) {
let self = this
self.orderId = ko.observable(qstr.id||qstr.order)
self.orderKey = ko.observable(qstr.key)
self.orderUrl = ko.pureComputed(() =>
`${SITE_DOMAIN}/order/?id=${self.orderId()}&key=${self.orderKey().replace(/ /g,'+')}`
)
self.items = ko.observableArray([])
self.shipname = ko.observable('')
self.contactname = ko.observable('')
@ -121,9 +125,9 @@ class Checkout { constructor(data) {
self.isSubmitted = ko.pureComputed( () =>
( self.submitted() >= self.totalxmr() )
)
self.paidDate = ko.observable('')
self.paidDate = ko.observable(new Date())
self.paidDate_pretty = ko.pureComputed(() =>
self.paidDate()
self.paidDate().toDateString()
)
self.isPaidUSD = ko.observable(false)
self.isPaid = ko.pureComputed(() =>
@ -141,7 +145,7 @@ class Checkout { constructor(data) {
)
self.transactions = ko.observableArray([])
self.xmr_uri = ko.pureComputed(() =>
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us_${self.orderId()}`
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us order ${self.orderId()}`
)
let xmr_qr = new QRCode(
document.getElementById('xmr_qr'),
@ -159,14 +163,23 @@ class Checkout { constructor(data) {
self.checkingStatus = ko.observable('')
self.titleStatus = ko.observable('')
self.stripeText = ko.observable('')
self.xmr_price = ko.observable(0)
self.submitStripePayment= ko.observable(()=>{})
self.shipDate = ko.observable(null)
self.shipDate_pretty = ko.pureComputed(() =>
(self.isShipped())?
self.shipDate().toDateString()
:''
)
self.shipCarrier = ko.observable('')
self.trackingNumbers = ko.observableArray([])
self.isShipped = ko.pureComputed(() =>
(self.shipDate()!=null)
)
;(async () => {
const getStripeStatus = async (secret, empty_cart=false) => {
// https://stripe.com/docs/error-handling?lang=node#use-stored-information
const { paymentIntent } = await stripe.retrievePaymentIntent(secret)
console.log(paymentIntent)
if (paymentIntent.last_payment_error !== null) {
if (paymentIntent.last_payment_error.type==='StripeCardError') {
self.titleStatus(' (declined)')
@ -181,7 +194,7 @@ class Checkout { constructor(data) {
} else {
if (paymentIntent.status==='succeeded') {
self.titleStatus(' (paid)')
self.stripeText(`Your order has been placed and your payment was processed. You may bookmark and/or print this page for your records. Your order number is: ${order.id}. You will receive a copy of this order by email. Tracking information will be sent when your order ships.`)
self.stripeText(`A ${self.total_pretty()} credit card payment for this order was processed ${(self.paidDate())?'on '+self.paidDate_pretty():''}. It will appear on your statement as "slvit.us ${self.orderId()}".`)
self.isPaidUSD(true)
if (empty_cart) emptyCart()
} else if (paymentIntent.status==='processing') {
@ -215,7 +228,7 @@ class Checkout { constructor(data) {
}
// Populate view model with order data
self.paidDate(order.paidDate)
self.paidDate(new Date(order.paidDate))
self.subtotal(formatUSD(order.subtotal))
self.shipping(formatUSD(order.shipping.amount))
self.processing(order.processing)
@ -243,6 +256,10 @@ class Checkout { constructor(data) {
self.contactname(order.contact.name)
self.phone(order.contact.phone)
self.email(order.contact.email)
self.shipDate( (order.shipping.date==null)?
null:new Date(order.shipping.date) )
self.shipCarrier(order.shipping.carrier)
self.trackingNumbers(order.shipping.tracking)
if (order.paymentMethod==='USD') {
self.isPaidUSD((qstr.redirect_status=='succeeded'||order.paidDate!=null))
@ -293,7 +310,7 @@ class Checkout { constructor(data) {
// Price checking
const getXmrPrice = async () => {
let res; try {
res = await fetch('https://localmonero.co/web/ticker?currencyCode=USD')
res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=usd')
} catch (err) {
return console.error(`Failed to fetch CoinGecko price: ${err}`)
}; let parsedRes; try {
@ -302,7 +319,7 @@ class Checkout { constructor(data) {
return console.error(`Error parsing CoinGecko response: ${err}`)
}
console.log(`Monero is now worth ${formatUSD(parsedRes.monero.usd)}`)
self.xmr_price(parsedRes.monero.usd)
xmr_price = parsedRes.monero.usd
}; getXmrPrice()
setInterval(getXmrPrice, MONERO_PRICECHECK_SEC*1000)
@ -349,7 +366,8 @@ class Checkout { constructor(data) {
self.unlocked(resData.amount.covered.unlocked/1000000000000)
}
}; getTransactions(); setInterval(() => {
}; getTransactions()
if (!self.isPaid()) setInterval(() => {
if (poll_in<=0) {
self.checkingStatus('Checking for transactions...')
if (!isChecking) getTransactions()

View File

@ -20,7 +20,14 @@ title: Order
</style>
<link rel="stylesheet" href="/assets/shop.css">
<script src="https://js.stripe.com/v3/"></script>
<h1>📋 Order <span data-bind="text:orderId"></span> <span data-bind="text:titleStatus"></span></h1>
<h1>📋 Order <span data-bind="visible:isPaid,text:orderId"></span> <span data-bind="text:titleStatus"></span></h1>
<p data-bind="visible:isPaid">Your order number is <code data-bind="text:orderId">(loading)</code>. You may bookmark and/or print this page for your records. You can also share this secret link:</p>
<pre data-bind="visible:isPaid"><code data-bind="text:orderUrl"></code></pre>
<hr>
<table class="tal" style="width:100%"><thead><b>
<th>Item</th>
<th class="tar">Price</th>
@ -101,45 +108,59 @@ title: Order
<p data-bind="text:stripeText"></p>
{# Monero payment status #}
<p data-bind="visible:paymentMethod()=='XMR'">Your order is for <code data-bind="text:totalxmr_pretty"></code>.
<br><code data-bind="text:submitted_pretty"></code> have been submitted to the blockchain.
<br><code data-bind="text:unlocked_pretty"></code> have been unlocked.
</p>
<p data-bind="hidden:(paymentMethod()!='XMR'||isPaid)">Transactions you send will appear on this page when they are accepted into a block. <b><i>THIS COULD TAKE TEN MINUTES OR MORE</i></b> Transactions "unlock" after receiving 10 confirmations on the blockchain, each taking a few minutes each. Once the full <span data-bind="text:totalxmr_pretty"></span> are unlocked, we'll ship your order.
<p data-bind="hidden:(paymentMethod()!='XMR'||isPaid)">You don't have to wait for the payment to unlock. After sumbitting it in your wallet app, you can close this window. We sent a link to this page to the email specified, if you ever want to come back and check on the order.</p>
<p data-bind="visible:(paymentMethod()=='XMR'&&isPaid)">Your payment has been confirmed! We will ship your order as soon as possible and send an email with the tracking info.</p>
<p data-bind="visible:(paymentMethod()=='XMR'&&isOverpaid)">You <i>overpaid</I> us by <code data-bind="text:overpaidAmount_pretty"></code>! We would be happy to return the change to you at no extra cost. This process is done by hand so please email us{% if env.SALES_EMAIL %} at {{env.SALES_EMAIL}}{% endif %} to let us know that you overpaid. Be sure to include your order number, <code data-bind="text:orderId"></code>, as well as a monero address where you can receive the refund. </p>
<h3 data-bind="visible:(paymentMethod()=='XMR'&&transactions().length>0)">Transactions</h3>
<table data-bind="visible:(paymentMethod()=='XMR'&&transactions().length>0)">
<thead>
<th class="tal">Date</th>
<th class="tal">Time</th>
<th class="tal">Amt</th>
<th class="tal">Fee</th>
<th class="tal">Confs</th>
<th class="tal">Block</th>
<th class="tal">Stat</th>
</thead>
<tbody data-bind="foreach:transactions"><tr>
<td class="tal" data-bind="text:date"></td>
<td class="tal" data-bind="text:time"></td>
<td class="tal" data-bind="text:amount"></td>
<td class="tal" data-bind="text:fee_usd_pretty"></td>
<td class="tal" data-bind="text:confirmations"></td>
<td class="tal" data-bind="text:height"></td>
<td class="tal" style="cursor:default">
<span data-bind="visible:locked" title="LOCKED: Wait for 10 confirmations for this transaction to unlock">⏲️</span>
<span data-bind="visible:double_spend_seen" title="DOUBLE SPENT! Double-spend has been detected! This transaction is invalid. Contact {{metadata.sales.email}} to resolve.">⛔</span>
<span data-bind="hidden:(locked||double_spend_seen)" title="CONFIRMED! This transaction is valid and has been confirmed by the blockchain.">✅</span>
</td>
</tr></tbody>
</table>
<p><i data-bind="text:checkingStatus"></i></p>
<div data-bind="visible:paymentMethod()=='XMR'">
<p>Your order is for <code data-bind="text:totalxmr_pretty"></code>.
<br><code data-bind="text:submitted_pretty"></code> have been submitted to the blockchain.
<br><code data-bind="text:unlocked_pretty"></code> have been unlocked.
</p>
<div data-bind="hidden:isPaid">
<p>Transactions you send will appear on this page when they are accepted into a block. <b><i>THIS COULD TAKE TEN MINUTES OR MORE</i></b>. Transactions "unlock" after receiving 10 confirmations on the blockchain, each taking a few minutes each. Once the full <span data-bind="text:totalxmr_pretty"></span> are unlocked, we'll ship your order.
<p>You don't have to wait for the payment to unlock. After sumbitting it in your wallet app, you can close this window. We sent a link to this page to the email specified, if you ever want to come back and check on the order.</p>
</div><div data-bind="visible:isPaid">
<p>Your payment has been confirmed! We will ship your order as soon as possible and send an email with the tracking info.</p>
</div>
<p data-bind="visible:isOverpaid">You <i>overpaid</I> us by <code data-bind="text:overpaidAmount_pretty"></code>! We would be happy to return the change to you at no extra cost. This process is done by hand so please email us{% if env.SALES_EMAIL %} at {{env.SALES_EMAIL}}{% endif %} to let us know that you overpaid. Be sure to include your order number, <code data-bind="text:orderId"></code>, as well as a monero address where you can receive the refund. </p>
<h3 data-bind="visible:transactions().length>0">Transactions</h3>
<table data-bind="visible:transactions().length>0">
<thead>
<th class="tal">Date</th>
<th class="tal">Time</th>
<th class="tal">Amt</th>
<th class="tal">Fee</th>
<th class="tal">Confs</th>
<th class="tal">Block</th>
<th class="tal">Stat</th>
</thead>
<tbody data-bind="foreach:transactions"><tr>
<td class="tal" data-bind="text:date"></td>
<td class="tal" data-bind="text:time"></td>
<td class="tal" data-bind="text:amount"></td>
<td class="tal" data-bind="text:fee_usd_pretty"></td>
<td class="tal" data-bind="text:confirmations"></td>
<td class="tal" data-bind="text:height"></td>
<td class="tal" style="cursor:default">
<span data-bind="visible:locked" title="LOCKED: Wait for 10 confirmations for this transaction to unlock">⏲️</span>
<span data-bind="visible:double_spend_seen" title="DOUBLE SPENT! Double-spend has been detected! This transaction is invalid. Contact {{metadata.sales.email}} to resolve.">⛔</span>
<span data-bind="hidden:(locked||double_spend_seen)" title="CONFIRMED! This transaction is valid and has been confirmed by the blockchain.">✅</span>
</td>
</tr></tbody>
</table>
<p data-bind="hidden:isPaid"><i data-bind="text:checkingStatus"></i></p>
</div>
{# Shipment section #}
<div data-bind="visible:isPaid">
<hr>
<h2>📦 Shipment</h2>
<p data-bind="hidden:isShipped">Your order has not yet shipped. We will email you a tracking number when it goes out. You can also check back here for updates.</p>
<p data-bind="visible:isShipped">Your order was shipped on <span data-bind="text:shipDate_pretty"></span>. </p>
</div>
<script>
const stripe = Stripe('{{env.STRIPE_PUB}}')
const SUCCESS_URL = '{{env.SITE_DOMAIN}}/shop/order/'
const API_DOMAIN = '{{env.API_DOMAIN}}'
const SITE_DOMAIN = '{{env.SITE_DOMAIN}}'
const MONERO_PRICECHECK_SEC = {{env.MONERO_PRICECHECK_SEC}}
const MONERO_CHECKOUT_POLL_SECS = {{env.MONERO_CHECKOUT_POLL_SECS}}
</script>

View File

@ -146,7 +146,7 @@ module.exports = async (req, res) => {
}
const getProcessedOrder = () => new Promise(async (resolve, reject) => {
order.key = crypto.randomBytes(64).toString('base64')
order.key = crypto.randomBytes(32).toString('hex')
// Process stripe order
if (order.paymentMethod==='USD') {

View File

@ -14,7 +14,7 @@ module.exports = async (order) => {
to: order.contact.email,
replyTo: process.env.SALES_EMAIL,
subject: `Order ${order.id} for ${amt} has been processed`,
text: `Dear ${order.contact.name},\n\nThank you for your payment. We will ship your order as soon as possible and send the tracking info to this email address. For now, keep this copy of your order details as proof of your payment. \n\n---\n\nITEMS: \n${customer_mail_items_string}\nTOTALS: \nSubtotal: ${formatUSD(order.subtotal)}\nTax: ${formatUSD(order.taxAmount)}\nShipping: ${formatUSD(order.shipping.amount)}\nProcessing: ${formatUSD(order.processing)}\nTOTAL: ${formatUSD(order.total)}${(order.paymentMethod==='XMR')?'\nMONERO TOTAL: '+amt:''}\n\nCONTACT: \n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n\nSHIPPING ADDRESS:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\n---\n\nTo view your order info anytime, visit this page: ${process.env.SITE_URL}/shop/order/stripe/?order=${order.id}&key=${order.key}\n\nFor questions, comments, or to cancel this order, reply to this email or call 719-936-7778 x1. \n\nThank you!\n\n`,
text: `Dear ${order.contact.name},\n\nThank you for your payment. We will ship your order as soon as possible and send the tracking info to this email address. For now, keep this copy of your order details as proof of your payment. \n\n---\n\nITEMS: \n${customer_mail_items_string}\nTOTALS: \nSubtotal: ${formatUSD(order.subtotal)}\nTax: ${formatUSD(order.taxAmount)}\nShipping: ${formatUSD(order.shipping.amount)}\nProcessing: ${formatUSD(order.processing)}\nTOTAL: ${formatUSD(order.total)}${(order.paymentMethod==='XMR')?'\nMONERO TOTAL: '+amt:''}\n\nCONTACT: \n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n\nSHIPPING ADDRESS:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\n---\n\nTo view your order info anytime, visit this page: ${process.env.SITE_DOMAIN}/shop/order/stripe/?order=${order.id}&key=${order.key}\n\nFor questions, comments, or to cancel this order, reply to this email or call 719-936-7778 x1. \n\nThank you!\n`,
})
console.log(`Sent email ${customer_mail_res.messageId} to customer for order ${order.id}`)
} catch (err) { console.error(err) }

View File

@ -13,7 +13,7 @@ module.exports = async (order) => {
from: process.env.SALES_MAIL_FROM,
to: process.env.SALES_EMAIL,
subject: `Order for ${amt} processed`,
text: `Order ${order.id} needs ${order.items.length} items shipped: \n\n${sales_mail_items_string}\nSHIP TO:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\nCONTACT:\n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n`,
text: `Order ${order.id} needs ${order.items.length} items shipped: \n\n${sales_mail_items_string}\nSHIP TO:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\nCONTACT:\n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n\n---\n\nOrder page: ${process.env.SITE_DOMAIN}/shop/order/stripe/?order=${order.id}&key=${order.key}\n`,
})
console.log(`Sent email ${sales_mail_res.messageId} to sales team for order ${order.id}`)
} catch (err) { console.error(err) }