Got stripe payment working
This commit is contained in:
parent
0b72b39864
commit
2f5d7eac59
|
@ -16,3 +16,5 @@ _site/*
|
|||
**/*.bak
|
||||
# vim swaps
|
||||
**/*.swp
|
||||
# VScode
|
||||
.vscode/
|
||||
|
|
|
@ -271,30 +271,20 @@ class Cart { constructor() {
|
|||
}
|
||||
|
||||
// Send to server
|
||||
const res = await fetch(`${API_DOMAIN}/order`, {
|
||||
method: 'POST', mode: 'cors',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: ko.toJSON(order),
|
||||
})
|
||||
if (res.ok) { // paymentIntent created
|
||||
console.log(res)
|
||||
let parsedRes; try {
|
||||
parsedRes = await res.json()
|
||||
} catch (err) { console.error(`Failed to parse JSON: ${err}`) }
|
||||
console.log(typeof parsedRes)
|
||||
console.log(parsedRes.id)
|
||||
if (self.payment_method()==='USD')
|
||||
window.location = `/shop/checkout/stripe/?order=${parsedRes.id}`
|
||||
else if (self.payment_method()==='XMR')
|
||||
window.location = `/shop/order/monero/?order=${parsedRes.id}`
|
||||
else {
|
||||
alert(`Invalid payment method: ${self.payment_method()}`)
|
||||
self.isLoading(false)
|
||||
}
|
||||
} else { // paymentIntent.create() failed
|
||||
alert(await res.text())
|
||||
self.isLoading(false)
|
||||
}
|
||||
let parsedRes; try {
|
||||
const res = await fetch(`${API_DOMAIN}/order`, {
|
||||
method: 'POST', mode: 'cors',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: ko.toJSON(order),
|
||||
})
|
||||
if (!res.ok) throw await res.text()
|
||||
else try { parsedRes = await res.json()}
|
||||
catch (err) { throw `Failed to parse response:\n${err}` }
|
||||
} catch (err) {
|
||||
alert('Failed to create order! Please try again later')
|
||||
console.error(err)
|
||||
} finally { self.isLoading(false) }
|
||||
window.location = `/shop/order/?id=${parsedRes.id}&key=${parsedRes.key}`
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
const formatUSD = (v) => v.toLocaleString(undefined, {
|
||||
style: 'currency', currency: 'USD' })
|
||||
const qs = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
})
|
||||
const dom = {
|
||||
itemsTable: document.getElementById('items-table'),
|
||||
tax: document.getElementById('tax'),
|
||||
shipping: document.getElementById('shipping'),
|
||||
processing: document.getElementById('processing'),
|
||||
subtotal: document.getElementById('subtotal'),
|
||||
total: document.getElementById('total'),
|
||||
buttonAmount: document.getElementById('final-amount'),
|
||||
shippingAddress: document.getElementById('shipping-address'),
|
||||
contact: document.getElementById('contact'),
|
||||
submitBtn: document.getElementById('submit'),
|
||||
}
|
||||
|
||||
;(async (orderId) => {
|
||||
|
||||
// Fetch order from api
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/order/${orderId}`, {
|
||||
headers: {'Accept':'application/json'},
|
||||
})
|
||||
if (!res.ok) throw res.statusText
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch order ${orderId} from ${API_DOMAIN}\n${err}`)
|
||||
}; let order; try {
|
||||
order = await res.json()
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse JSON:\n${err}`)
|
||||
}
|
||||
|
||||
// Stripe payment form
|
||||
const elements = stripe.elements({
|
||||
appearance: {
|
||||
theme: 'night',
|
||||
variables: {
|
||||
colorPrimary: '#3d3d83', // Purple for tab selection
|
||||
colorBackground: '#212121',
|
||||
colorText: '#e0e0e0',
|
||||
colorTextSecondary: '#e0e0e0',
|
||||
colorLogo: 'dark',
|
||||
fontFamily: 'dejavu, monospace',
|
||||
},
|
||||
},
|
||||
clientSecret: order.stripe_secret,
|
||||
})
|
||||
const paymentElement = elements.create('payment', {
|
||||
paymentMethodOrder: ['card'],
|
||||
layout: 'tabs', // or 'accordian'
|
||||
})
|
||||
paymentElement.mount('#payment-element')
|
||||
|
||||
// Populate page with the data
|
||||
dom.tax.innerText = formatUSD(order.taxAmount)
|
||||
dom.shipping.innerText = formatUSD(order.shipping.amount)
|
||||
dom.processing.innerText = formatUSD(order.processing)
|
||||
dom.subtotal.innerText = formatUSD(order.subtotal)
|
||||
dom.total.innerText = formatUSD(order.total)
|
||||
dom.buttonAmount.innerText = ` for ${formatUSD(order.total)}`
|
||||
dom.shippingAddress.innerHTML = `${order.shipping.address.name}<br>${order.shipping.address.addr1}<br>${(order.shipping.address.addr2)?order.shipping.address.addr2+'<br>':''}${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}`
|
||||
dom.contact.innerHTML = `${order.contact.name}<br>${order.contact.phone}<br>${order.contact.email}`
|
||||
order.items.forEach((item) => {
|
||||
dom.itemsTable.innerHTML += `<tr><td>${item.name}</td>
|
||||
<td style="text-align:right">${formatUSD(parseFloat(item.price))}</td>
|
||||
<td style="width:2rem;text-align:right">${item.qty}</td>
|
||||
<td style="text-align:right">${formatUSD(item.subtotal)}</td></tr>`
|
||||
})
|
||||
|
||||
})(qs.order)
|
||||
|
||||
// onClick cancel link
|
||||
const cancel = () => window.location = '/shop/cart/'
|
||||
|
||||
// onClick submit button
|
||||
const submitPayment = async () => {
|
||||
const oldBtnText = dom.submitBtm.innerText
|
||||
dom.submitBtn.innerText = 'Placing order...'
|
||||
dom.submitBtn.setAttribute('disabled','disabled')
|
||||
const { error } = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: SUCCESS_URL,
|
||||
},
|
||||
})
|
||||
if (error) {
|
||||
alert(error.message)
|
||||
dom.submitBtn.innerText = oldBtnText
|
||||
dom.submitBtn.removeAttribute('disabled')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
'use strict'
|
||||
const formatUSD = (v) => v.toLocaleString(undefined, {
|
||||
style: 'currency', currency: 'USD' })
|
||||
const cancel = () => window.location = '/shop/cart/'
|
||||
const qstr = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
})
|
||||
const STRIPE_DECLINE_CODES = {
|
||||
generic_decline: `The card was declined for an unknown reason. You need to to contact your card-issuer for more information.`,
|
||||
authentication_required: `The card was declined as the transaction requires authentication. You should try again and authenticate your card when prompted during the transaction. If the transaction <i>was</i> authenticated, contact your card-issuer for more information.`,
|
||||
approve_with_id: `The payment can’t be authorized. Attempt the payment again. If you still can’t process it, contact your card-issuer.`,
|
||||
call_issuer: `The card was declined for an unknown reason. Contact your card-issuer for more information.`,
|
||||
card_not_supported: `The card does not support this type of purchase. You can contact your card-issuer to make sure your card can be used to make this type of purchase.`,
|
||||
card_velocity_exceeded: `You have exceeded the balance or credit limit available on your card. You can to contact your card-issuer for more information.`,
|
||||
currency_not_supported: `The card does not support the specified currency. You need to check with the issuer whether the card can be used for the type of currency specified.`,
|
||||
do_not_honor: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
do_not_try_again: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
duplicate_transaction: `A transaction with identical amount and credit card information was submitted very recently. Check to see if a recent payment already exists.`,
|
||||
expired_card: `The card has expired. You need to use another card.`,
|
||||
incorrect_number: `The card number is incorrect. You need to try again using the correct card number.`,
|
||||
incorrect_cvc: `The CVC number is incorrect. You need to try again using the correct CVC.`,
|
||||
incorrect_pin: `The PIN entered is incorrect. This decline code only applies to payments made with a card reader. You need to try again using the correct PIN.`,
|
||||
incorrect_zip: `The postal code is incorrect. You need to try again using the correct billing postal code.`,
|
||||
insufficient_funds: `The card has insufficient funds to complete the purchase. You need to use an alternative payment method.`,
|
||||
invalid_account: `The card, or account the card is connected to, is invalid. You need to contact your card-issuer to check that the card is working correctly.`,
|
||||
invalid_amount: `The payment amount is invalid, or exceeds the amount that’s allowed. If the amount appears to be correct, You need to check with your card-issuer that they can make purchases of that amount.`,
|
||||
invalid_cvc: `The CVC number is incorrect. You need to try again using the correct CVC.`,
|
||||
invalid_expiry_month: `The expiration month is invalid. You need to try again using the correct expiration date.`,
|
||||
invalid_expiry_year: `The expiration year is invalid. You need try again using the correct expiration date.`,
|
||||
invalid_number: `The card number is incorrect. You need try again using the correct card number.`,
|
||||
invalid_pin: `The PIN entered is incorrect. This decline code only applies to payments made with a card reader. You need to try again using the correct PIN.`,
|
||||
issuer_not_available: `The card-issuer couldn’t be reached, so the payment couldn’t be authorized. Attempt the payment again. If you still can’t process it, You need to contact your card-issuer.`,
|
||||
new_account_information_available: `The card, or account the card is connected to, is invalid. You need to contact your card-issuer for more information.`,
|
||||
no_action_taken: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
not_permitted: `The payment isn’t permitted. You need to contact your card-issuer for more information.`,
|
||||
offline_pin_required: `The card was declined because it requires a PIN. You need to try again by inserting your card and entering a PIN.`,
|
||||
online_or_offline_pin_required: `The card was declined as it requires a PIN. If the card reader supports Online PIN, prompt the customer for a PIN without creating a new transaction. If the card reader doesn’t support Online PIN, You need to try again by inserting your card and entering a PIN.`,
|
||||
pickup_card: `The customer can’t use this card to make this payment (it’s possible it was reported lost or stolen). They need to contact your card-issuer for more information.`,
|
||||
pin_try_exceeded: `The allowable number of PIN tries was exceeded. The customer must use another card or method of payment.`,
|
||||
processing_error: `An error occurred while processing the card. The payment needs to be attempted again. If it still can’t be processed, try again later.`,
|
||||
reenter_transaction: `The payment couldn’t be processed by the issuer for an unknown reason. The payment needs to be attempted again. If it still can’t be processed, You need to contact your card-issuer.`,
|
||||
restricted_card: `The customer can’t use this card to make this payment (it’s possible it was reported lost or stolen). You need to contact your card-issuer for more information.`,
|
||||
revocation_of_all_authorizations: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
revocation_of_authorization: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
security_violation: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
service_not_allowed: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
stop_payment_order: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
testmode_decline: `A Stripe test card number was used. A genuine card must be used to make a payment.`,
|
||||
transaction_not_allowed: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
try_again_later: `The card was declined for an unknown reason. Ask the customer to attempt the payment again. If subsequent payments are declined, You need to contact your card-issuer for more information.`,
|
||||
withdrawal_count_limit_exceeded: `The customer has exceeded the balance or credit limit available on your card. You need to use an alternative payment method.`,
|
||||
}
|
||||
const emptyCart = () => {
|
||||
Object.keys(localStorage).forEach(i => {
|
||||
if (i.substring(0,5)==='cart_')
|
||||
localStorage.removeItem(i)
|
||||
}); recountCart()
|
||||
}
|
||||
|
||||
//let socket
|
||||
|
||||
class MoneroTransaction { constructor(data) {
|
||||
let self = this
|
||||
self.amount = data.amount/1000000000000
|
||||
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.height = data.height
|
||||
self.datetime = new Date(data.timestamp)
|
||||
self.date = self.datetime.toLocaleDateString()
|
||||
self.time = self.datetime.toLocaleTimeString()
|
||||
self.tx_hash = data.tx_hash
|
||||
self.unlock_time = ko.observable(0)
|
||||
self.locked = ko.observable(true)
|
||||
} }
|
||||
|
||||
class Checkout { constructor(data) {
|
||||
let self = this
|
||||
self.orderId = ko.observable(qstr.id||qstr.order)
|
||||
self.orderKey = ko.observable(qstr.key)
|
||||
self.items = ko.observableArray([])
|
||||
self.shipname = ko.observable('')
|
||||
self.contactname = ko.observable('')
|
||||
self.phone = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.addr1 = ko.observable('')
|
||||
self.addr2 = ko.observable('')
|
||||
self.city = ko.observable('')
|
||||
self.zip = ko.observable('')
|
||||
self.state = ko.observable('')
|
||||
self.tax = ko.observable(0)
|
||||
self.shipping = ko.observable(0)
|
||||
self.processing = ko.observable(0)
|
||||
self.processing_pretty = ko.computed(() =>
|
||||
formatUSD(self.processing())
|
||||
)
|
||||
self.subtotal = ko.observable(0)
|
||||
self.total = ko.observable(0)
|
||||
self.total_pretty = ko.computed(() =>
|
||||
formatUSD(self.total())
|
||||
)
|
||||
self.paymentMethod = ko.observable('')
|
||||
self.totalxmr = ko.observable(0)
|
||||
self.totalxmr_pretty = ko.pureComputed(() =>
|
||||
`${self.totalxmr()} XMR`
|
||||
)
|
||||
self.xmr_address = ko.observable('')
|
||||
self.submitted = ko.observable(0)
|
||||
self.submitted_pretty = ko.pureComputed( () =>
|
||||
`${self.submitted()} XMR`
|
||||
)
|
||||
self.unsubmitted = ko.pureComputed( () =>
|
||||
self.totalxmr() - self.submitted()
|
||||
)
|
||||
self.unlocked = ko.observable(0)
|
||||
self.unlocked_pretty = ko.pureComputed( () =>
|
||||
`${self.unlocked()} XMR`
|
||||
)
|
||||
self.isSubmitted = ko.pureComputed( () =>
|
||||
( self.submitted() >= self.totalxmr() )
|
||||
)
|
||||
self.paidDate = ko.observable('')
|
||||
self.paidDate_pretty = ko.pureComputed(() =>
|
||||
self.paidDate()
|
||||
)
|
||||
self.isPaidUSD = ko.observable(false)
|
||||
self.isPaid = ko.pureComputed(() =>
|
||||
(self.isPaidUSD() || // USD paid
|
||||
( self.unlocked() >= self.totalxmr() )) // XMR paid
|
||||
)
|
||||
self.isOverpaid = ko.pureComputed( () =>
|
||||
( self.submitted() > self.totalxmr() )
|
||||
)
|
||||
self.overpaidAmount = ko.pureComputed( () =>
|
||||
(self.isOverpaid)? self.submitted() - self.totalxmr() : 0
|
||||
)
|
||||
self.overpaidAmount_pretty = ko.pureComputed( () =>
|
||||
`${self.overpaidAmount()} XMR`
|
||||
)
|
||||
self.transactions = ko.observableArray([])
|
||||
self.xmr_uri = ko.pureComputed(() =>
|
||||
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us_${self.orderId()}`
|
||||
)
|
||||
let xmr_qr = new QRCode(
|
||||
document.getElementById('xmr_qr'),
|
||||
self.xmr_uri()
|
||||
)
|
||||
self.xmr_uri.subscribe(() => {
|
||||
xmr_qr.clear()
|
||||
xmr_qr.makeCode(self.xmr_uri())
|
||||
})
|
||||
self.isPlacingStripeOrder = ko.observable(false)
|
||||
self.stripeSubmitButtonText = ko.computed(() =>
|
||||
(self.isPlacingStripeOrder())?'Placing order...':
|
||||
`Place order for ${self.total_pretty()}`
|
||||
)
|
||||
self.checkingStatus = ko.observable('')
|
||||
self.titleStatus = ko.observable('')
|
||||
self.stripeText = ko.observable('')
|
||||
self.xmr_price = ko.observable(0)
|
||||
self.submitStripePayment= ko.observable(()=>{})
|
||||
|
||||
;(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)')
|
||||
self.stripeText(`Your payment was declined. Our payment processor, Stripe, sent back this error: <code>${paymentIntent.last_payment_error.code||'generic_decline'}</code>. Here's what they think that means: </p>
|
||||
<blockquote>${DECLINE_CODES[paymentIntent.last_payment_error.code||'generic_decline']||DECLINE_CODES['generic_decline']}</blockquote>
|
||||
<p>What you do now is up to you. Maybe you want to <a href="/shop/checkout/stripe/">go back to the checkout page</a> and try again.`)
|
||||
} else {
|
||||
self.titleStatus(' (failed)')
|
||||
self.stripeText(`The payment failed due to an ${paymentIntent.last_payment_error.type} error.`)
|
||||
}
|
||||
self.isPaidUSD(false)
|
||||
} 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.isPaidUSD(true)
|
||||
if (empty_cart) emptyCart()
|
||||
} else if (paymentIntent.status==='processing') {
|
||||
self.titleStatus(' (processing)')
|
||||
self.stripeText(`Your payment wasn't processed <i>yet</i>. Wait a few minutes and then try <a href="javascript:window.location.reload(true)">refreshing this page</a>. `)
|
||||
self.isPaidUSD(false)
|
||||
} else {
|
||||
self.titleStatus(' (failed)')
|
||||
self.stripeText('The payment failed due to an unknown error.')
|
||||
self.isPaidUSD(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check stripe status first if possible
|
||||
if (qstr.payment_intent_client_secret!=null)
|
||||
getStripeStatus(qstr.payment_intent_client_secret)
|
||||
|
||||
// Fetch order from API
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/order/${qstr.id}?key=${qstr.key}`, {
|
||||
headers: {'Accept':'application/json'},
|
||||
})
|
||||
if (!res.ok) throw res.statusText
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch order from API!\n${err}`)
|
||||
}; let order; try {
|
||||
order = await res.json()
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse JSON:\n${err}`)
|
||||
}
|
||||
|
||||
// Populate view model with order data
|
||||
self.paidDate(order.paidDate)
|
||||
self.subtotal(formatUSD(order.subtotal))
|
||||
self.shipping(formatUSD(order.shipping.amount))
|
||||
self.processing(order.processing)
|
||||
self.processing_pretty = ko.pureComputed(() =>
|
||||
self.processing()
|
||||
)
|
||||
self.tax(formatUSD(order.taxAmount))
|
||||
self.total(formatUSD(order.total))
|
||||
self.paymentMethod(order.paymentMethod)
|
||||
self.totalxmr(order.totalxmr)
|
||||
order.items.forEach((item) =>
|
||||
self.items.push({
|
||||
name: item.name,
|
||||
price: formatUSD(parseFloat(item.price)),
|
||||
qty: item.qty,
|
||||
subtotal: formatUSD(item.subtotal),
|
||||
})
|
||||
)
|
||||
self.shipname(order.shipping.address.name)
|
||||
self.addr1(order.shipping.address.addr1)
|
||||
self.addr2(order.shipping.address.addr2)
|
||||
self.city(order.shipping.address.city)
|
||||
self.zip(order.shipping.address.zip)
|
||||
self.state(order.shipping.address.state)
|
||||
self.contactname(order.contact.name)
|
||||
self.phone(order.contact.phone)
|
||||
self.email(order.contact.email)
|
||||
|
||||
if (order.paymentMethod==='USD') {
|
||||
self.isPaidUSD((qstr.redirect_status=='succeeded'||order.paidDate!=null))
|
||||
|
||||
// Try to get payment status if we didn't already
|
||||
if (self.isPaidUSD()) {
|
||||
if (qstr.payment_intent_client_secret==null)
|
||||
getStripeStatus(order.stripe_secret)
|
||||
} else {
|
||||
// Stripe payment form
|
||||
const elements = stripe.elements({
|
||||
clientSecret: order.stripe_secret,
|
||||
appearance: {
|
||||
theme: 'night',
|
||||
variables: {
|
||||
colorPrimary: '#3d3d83',
|
||||
colorBackground: '#212121',
|
||||
colorText: '#e0e0e0',
|
||||
colorTextSecondary: '#e0e0e0',
|
||||
colorLogo: 'dark',
|
||||
fontFamily: 'dejavu, monospace',
|
||||
},
|
||||
},
|
||||
})
|
||||
elements.create('payment', {
|
||||
paymentMethodOrder: ['card'],
|
||||
}).mount('#stripe-payment')
|
||||
self.submitStripePayment(async () => {
|
||||
self.isPlacingStripeOrder(true)
|
||||
// const { error } = await stripe.confirmPayment({
|
||||
// elements,
|
||||
// confirmParams: { return_url: SUCCESS_URL },
|
||||
// })
|
||||
let payment_intent; try {
|
||||
payment_intent = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: { return_url: `${SUCCESS_URL}?id=${order.id}&key=${order.key}` },
|
||||
})
|
||||
if (payment_intent.error) throw payment_intent.error
|
||||
} catch (err) { alert(err.message) }
|
||||
finally { self.isPlacingStripeOrder(false) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
else if (order.paymentMethod==='XMR') {
|
||||
|
||||
// Price checking
|
||||
const getXmrPrice = async () => {
|
||||
let res; try {
|
||||
res = await fetch('https://localmonero.co/web/ticker?currencyCode=USD')
|
||||
} catch (err) {
|
||||
return console.error(`Failed to fetch CoinGecko price: ${err}`)
|
||||
}; let parsedRes; try {
|
||||
parsedRes = await res.json()
|
||||
} catch (err) {
|
||||
return console.error(`Error parsing CoinGecko response: ${err}`)
|
||||
}
|
||||
console.log(`Monero is now worth ${formatUSD(parsedRes.monero.usd)}`)
|
||||
self.xmr_price(parsedRes.monero.usd)
|
||||
}; getXmrPrice()
|
||||
setInterval(getXmrPrice, MONERO_PRICECHECK_SEC*1000)
|
||||
|
||||
self.xmr_address(order.xmr_address)
|
||||
self.totalxmr(order.totalxmr)
|
||||
|
||||
// Poll for new xmr transactions
|
||||
let poll_in = MONERO_CHECKOUT_POLL_SECS
|
||||
let isChecking = false
|
||||
const getTransactions = async () => {
|
||||
isChecking = true
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/xmr-receive/${order.xmr_address}`)
|
||||
} catch (err) {
|
||||
return console.error(`Failed to get update about xmr address ${order.xmr_address}\n${err}`)
|
||||
} if (!res.ok)
|
||||
return console.error(`Got a bad response when requesting update on this xmr address: ${res.status}\n${res}`)
|
||||
let resData; try {
|
||||
resData = await res.json()
|
||||
} catch (err) {
|
||||
return console.error(`Failed to parse response JSON: ${err}`)
|
||||
} finally {
|
||||
isChecking = false
|
||||
poll_in = MONERO_CHECKOUT_POLL_SECS
|
||||
}
|
||||
if (resData.transactions) {
|
||||
resData.transactions.forEach( (tx) => {
|
||||
const existingTransactions = self.transactions()
|
||||
.filter((i) => (i.tx_hash===tx.tx_hash))
|
||||
if (existingTransactions.length) {
|
||||
existingTransactions[0].confirmations(tx.confirmations)
|
||||
existingTransactions[0].double_spend_seen(tx.double_spend_seen)
|
||||
existingTransactions[0].unlock_time(tx.unlock_time)
|
||||
existingTransactions[0].locked(tx.locked)
|
||||
} else {
|
||||
self.transactions.push(new MoneroTransaction(tx))
|
||||
self.transactions.sort( (a,b) =>
|
||||
(a.height===b.height)?0
|
||||
:(a.height<b.height)?-1:1
|
||||
)
|
||||
}
|
||||
})
|
||||
self.submitted(resData.amount.covered.total/1000000000000)
|
||||
self.unlocked(resData.amount.covered.unlocked/1000000000000)
|
||||
}
|
||||
|
||||
}; getTransactions(); setInterval(() => {
|
||||
if (poll_in<=0) {
|
||||
self.checkingStatus('Checking for transactions...')
|
||||
if (!isChecking) getTransactions()
|
||||
} else {
|
||||
self.checkingStatus(`Checking for transactions in ${
|
||||
poll_in--}...`)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Websockets
|
||||
// socket = io(API_DOMAIN,{query:{orderid:data.id}})
|
||||
// socket.on('mp-cb', (body) => {
|
||||
// console.log(body)
|
||||
// body.transactions.forEach( (tx) => {
|
||||
// const existingTransactions = self.transactions()
|
||||
// .filter((i) => (i.tx_hash===tx.tx_hash))
|
||||
// if (existingTransactions.length>0) {
|
||||
// existingTransactions[0].confirmations(tx.confirmations)
|
||||
// existingTransactions[0].double_spend_seen(tx.double_spend_seen)
|
||||
// existingTransactions[0].unlock_time(tx.unlock_time)
|
||||
// existingTransactions[0].locked(tx.locked)
|
||||
// } else self.transactions.push(tx)
|
||||
// })
|
||||
// self.submitted(body.amount.covered.total)
|
||||
// self.unlocked(body.amount.covered.unlocked)
|
||||
// })
|
||||
|
||||
}
|
||||
|
||||
else alert(
|
||||
`Invalid payment method in order: ${order.paymentMethod}`
|
||||
)
|
||||
|
||||
} )()
|
||||
|
||||
} }; ko.applyBindings(new Checkout())
|
|
@ -1,232 +0,0 @@
|
|||
'use strict'
|
||||
const formatUSD = (v) => v.toLocaleString(undefined, {
|
||||
style: 'currency', currency: 'USD' })
|
||||
const cancel = () => window.location = '/shop/cart/'
|
||||
// Parse querystring (https://stackoverflow.com/a/901144/)
|
||||
const qs = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
})
|
||||
//let socket
|
||||
|
||||
class moneroCheckoutItem { constructor(data) {
|
||||
let self = this
|
||||
self.name = data.name
|
||||
self.price = data.price
|
||||
self.qty = data.qty
|
||||
self.subtotal = data.subtotal
|
||||
} }
|
||||
|
||||
class moneroCheckoutTransaction { constructor(data) {
|
||||
let self = this
|
||||
self.amount = data.amount/1000000000000
|
||||
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.height = data.height
|
||||
self.datetime = new Date(data.timestamp)
|
||||
self.date = self.datetime.toLocaleDateString()
|
||||
self.time = self.datetime.toLocaleTimeString()
|
||||
self.tx_hash = data.tx_hash
|
||||
self.unlock_time = ko.observable(0)
|
||||
self.locked = ko.observable(true)
|
||||
} }
|
||||
|
||||
class moneroCheckout { constructor(data) {
|
||||
let self = this
|
||||
self.items = ko.observableArray([])
|
||||
self.shipname = ko.observable('')
|
||||
self.contactname = ko.observable('')
|
||||
self.phone = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.addr1 = ko.observable('')
|
||||
self.addr2 = ko.observable('')
|
||||
self.city = ko.observable('')
|
||||
self.zip = ko.observable('')
|
||||
self.state = ko.observable('')
|
||||
self.tax = ko.observable(0)
|
||||
self.shipping = ko.observable(0)
|
||||
self.subtotal = ko.observable(0)
|
||||
self.total = ko.observable(0)
|
||||
self.totalxmr = ko.observable(0)
|
||||
self.totalxmr_pretty = ko.pureComputed(() =>
|
||||
`${self.totalxmr()} XMR`
|
||||
)
|
||||
self.orderid = ko.observable('')
|
||||
self.xmr_address = ko.observable('')
|
||||
self.submitted = ko.observable(0)
|
||||
self.submitted_pretty = ko.pureComputed( () =>
|
||||
`${self.submitted()} XMR`
|
||||
)
|
||||
self.unsubmitted = ko.pureComputed( () =>
|
||||
self.totalxmr() - self.submitted()
|
||||
)
|
||||
self.unlocked = ko.observable(0)
|
||||
self.unlocked_pretty = ko.pureComputed( () =>
|
||||
`${self.unlocked()} XMR`
|
||||
)
|
||||
self.isSubmitted = ko.pureComputed( () =>
|
||||
( self.submitted() >= self.totalxmr() )
|
||||
)
|
||||
self.isPaid = ko.pureComputed( () =>
|
||||
( self.unlocked() >= self.totalxmr() )
|
||||
)
|
||||
self.isOverpaid = ko.pureComputed( () =>
|
||||
( self.submitted() > self.totalxmr() )
|
||||
)
|
||||
self.overpaidAmount = ko.pureComputed( () =>
|
||||
(self.isOverpaid)? self.submitted() - self.totalxmr() : 0
|
||||
)
|
||||
self.overpaidAmount_pretty = ko.pureComputed( () =>
|
||||
`${self.overpaidAmount()} XMR`
|
||||
)
|
||||
self.transactions = ko.observableArray([])
|
||||
self.xmr_uri = ko.pureComputed(() =>
|
||||
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us_${self.orderid()}`
|
||||
)
|
||||
let xmr_qr = new QRCode(
|
||||
document.getElementById('xmr_qr'),
|
||||
self.xmr_uri()
|
||||
)
|
||||
self.xmr_uri.subscribe(() => {
|
||||
xmr_qr.clear()
|
||||
xmr_qr.makeCode(self.xmr_uri())
|
||||
})
|
||||
self.checkingStatus = ko.observable('')
|
||||
|
||||
// Monero pricing
|
||||
self.xmr_price = ko.observable(0)
|
||||
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 {
|
||||
parsedRes = await res.json()
|
||||
} catch (err) {
|
||||
return console.error(`Error parsing CoinGecko response: ${err}`)
|
||||
}
|
||||
console.log(`Monero is now worth ${formatUSD(parsedRes.monero.usd)}`)
|
||||
self.xmr_price(parsedRes.monero.usd)
|
||||
//self.xmr_price(parsedRes.USD.avg_24h)
|
||||
}; getXmrPrice()
|
||||
setInterval(getXmrPrice, MONERO_PRICECHECK_SEC*1000)
|
||||
|
||||
// Get order info from querystring
|
||||
self.orderid(qs.order)
|
||||
|
||||
;(async () => {
|
||||
|
||||
// Fetch order from API
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/order/${qs.order}`, {
|
||||
headers: {'Accept':'application/json'},
|
||||
})
|
||||
if (!res.ok) throw res.statusText
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch order from API!\n${err}`)
|
||||
}; let order; try {
|
||||
order = await res.json()
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse JSON:\n${err}`)
|
||||
}
|
||||
|
||||
// Populate DOM with order
|
||||
self.xmr_address(order.xmr_address)
|
||||
self.subtotal(formatUSD(order.subtotal))
|
||||
self.shipping(formatUSD(order.shipping.amount))
|
||||
self.tax(formatUSD(order.taxAmount))
|
||||
self.total(formatUSD(order.total))
|
||||
self.orderid(order.id)
|
||||
self.totalxmr(order.totalxmr)
|
||||
order.items.forEach((item) =>
|
||||
self.items.push({
|
||||
name: item.name,
|
||||
price: formatUSD(parseFloat(item.price)),
|
||||
qty: item.qty,
|
||||
subtotal: formatUSD(item.subtotal),
|
||||
})
|
||||
)
|
||||
self.shipname(order.shipping.address.name)
|
||||
self.addr1(order.shipping.address.addr1)
|
||||
self.addr2(order.shipping.address.addr2)
|
||||
self.city(order.shipping.address.city)
|
||||
self.zip(order.shipping.address.zip)
|
||||
self.state(order.shipping.address.state)
|
||||
self.contactname(order.contact.name)
|
||||
self.phone(order.contact.phone)
|
||||
self.email(order.contact.email)
|
||||
|
||||
// Poll for new transactions
|
||||
let poll_in = MONERO_CHECKOUT_POLL_SECS
|
||||
let isChecking = false
|
||||
const getTransactions = async () => {
|
||||
isChecking = true
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/xmr-receive/${order.xmr_address}`)
|
||||
} catch (err) {
|
||||
return console.error(`Failed to get update about xmr address ${order.xmr_address}\n${err}`)
|
||||
} if (!res.ok)
|
||||
return console.error(`Got a bad response when requesting update on this xmr address: ${res.status}\n${res}`)
|
||||
let resData; try {
|
||||
resData = await res.json()
|
||||
} catch (err) {
|
||||
return console.error(`Failed to parse response JSON: ${err}`)
|
||||
} finally {
|
||||
isChecking = false
|
||||
poll_in = MONERO_CHECKOUT_POLL_SECS
|
||||
}
|
||||
if (resData.transactions) {
|
||||
resData.transactions.forEach( (tx) => {
|
||||
const existingTransactions = self.transactions()
|
||||
.filter((i) => (i.tx_hash===tx.tx_hash))
|
||||
if (existingTransactions.length) {
|
||||
existingTransactions[0].confirmations(tx.confirmations)
|
||||
existingTransactions[0].double_spend_seen(tx.double_spend_seen)
|
||||
existingTransactions[0].unlock_time(tx.unlock_time)
|
||||
existingTransactions[0].locked(tx.locked)
|
||||
} else {
|
||||
self.transactions.push(new moneroCheckoutTransaction(tx))
|
||||
self.transactions.sort( (a,b) =>
|
||||
(a.height===b.height)?0
|
||||
:(a.height<b.height)?-1:1
|
||||
)
|
||||
}
|
||||
})
|
||||
self.submitted(resData.amount.covered.total/1000000000000)
|
||||
self.unlocked(resData.amount.covered.unlocked/1000000000000)
|
||||
}
|
||||
|
||||
}; getTransactions(); setInterval(() => {
|
||||
if (poll_in<=0) {
|
||||
self.checkingStatus('Checking for transactions...')
|
||||
if (!isChecking) getTransactions()
|
||||
} else {
|
||||
self.checkingStatus(`Checking for transactions in ${
|
||||
poll_in--}...`)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Websockets
|
||||
// socket = io(API_DOMAIN,{query:{orderid:data.id}})
|
||||
// socket.on('mp-cb', (body) => {
|
||||
// console.log(body)
|
||||
// body.transactions.forEach( (tx) => {
|
||||
// const existingTransactions = self.transactions()
|
||||
// .filter((i) => (i.tx_hash===tx.tx_hash))
|
||||
// if (existingTransactions.length>0) {
|
||||
// existingTransactions[0].confirmations(tx.confirmations)
|
||||
// existingTransactions[0].double_spend_seen(tx.double_spend_seen)
|
||||
// existingTransactions[0].unlock_time(tx.unlock_time)
|
||||
// existingTransactions[0].locked(tx.locked)
|
||||
// } else self.transactions.push(tx)
|
||||
// })
|
||||
// self.submitted(body.amount.covered.total)
|
||||
// self.unlocked(body.amount.covered.unlocked)
|
||||
// })
|
||||
|
||||
} )()
|
||||
|
||||
} }; ko.applyBindings(new moneroCheckout())
|
|
@ -1,156 +0,0 @@
|
|||
const formatUSD = (v) => v.toLocaleString(undefined, {
|
||||
style: 'currency', currency: 'USD' })
|
||||
// Parse querystring (https://stackoverflow.com/a/901144/)
|
||||
const qs = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
})
|
||||
|
||||
// See https://stripe.com/docs/declines/codes#generic_decline
|
||||
const DECLINE_CODES = {
|
||||
generic_decline: `The card was declined for an unknown reason. You need to to contact your card-issuer for more information.`,
|
||||
authentication_required: `The card was declined as the transaction requires authentication. You should try again and authenticate your card when prompted during the transaction. If the transaction <i>was</i> authenticated, contact your card-issuer for more information.`,
|
||||
approve_with_id: `The payment can’t be authorized. Attempt the payment again. If you still can’t process it, contact your card-issuer.`,
|
||||
call_issuer: `The card was declined for an unknown reason. Contact your card-issuer for more information.`,
|
||||
card_not_supported: `The card does not support this type of purchase. You can contact your card-issuer to make sure your card can be used to make this type of purchase.`,
|
||||
card_velocity_exceeded: `You have exceeded the balance or credit limit available on your card. You can to contact your card-issuer for more information.`,
|
||||
currency_not_supported: `The card does not support the specified currency. You need to check with the issuer whether the card can be used for the type of currency specified.`,
|
||||
do_not_honor: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
do_not_try_again: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
duplicate_transaction: `A transaction with identical amount and credit card information was submitted very recently. Check to see if a recent payment already exists.`,
|
||||
expired_card: `The card has expired. You need to use another card.`,
|
||||
incorrect_number: `The card number is incorrect. You need to try again using the correct card number.`,
|
||||
incorrect_cvc: `The CVC number is incorrect. You need to try again using the correct CVC.`,
|
||||
incorrect_pin: `The PIN entered is incorrect. This decline code only applies to payments made with a card reader. You need to try again using the correct PIN.`,
|
||||
incorrect_zip: `The postal code is incorrect. You need to try again using the correct billing postal code.`,
|
||||
insufficient_funds: `The card has insufficient funds to complete the purchase. You need to use an alternative payment method.`,
|
||||
invalid_account: `The card, or account the card is connected to, is invalid. You need to contact your card-issuer to check that the card is working correctly.`,
|
||||
invalid_amount: `The payment amount is invalid, or exceeds the amount that’s allowed. If the amount appears to be correct, You need to check with your card-issuer that they can make purchases of that amount.`,
|
||||
invalid_cvc: `The CVC number is incorrect. You need to try again using the correct CVC.`,
|
||||
invalid_expiry_month: `The expiration month is invalid. You need to try again using the correct expiration date.`,
|
||||
invalid_expiry_year: `The expiration year is invalid. You need try again using the correct expiration date.`,
|
||||
invalid_number: `The card number is incorrect. You need try again using the correct card number.`,
|
||||
invalid_pin: `The PIN entered is incorrect. This decline code only applies to payments made with a card reader. You need to try again using the correct PIN.`,
|
||||
issuer_not_available: `The card-issuer couldn’t be reached, so the payment couldn’t be authorized. Attempt the payment again. If you still can’t process it, You need to contact your card-issuer.`,
|
||||
new_account_information_available: `The card, or account the card is connected to, is invalid. You need to contact your card-issuer for more information.`,
|
||||
no_action_taken: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
not_permitted: `The payment isn’t permitted. You need to contact your card-issuer for more information.`,
|
||||
offline_pin_required: `The card was declined because it requires a PIN. You need to try again by inserting your card and entering a PIN.`,
|
||||
online_or_offline_pin_required: `The card was declined as it requires a PIN. If the card reader supports Online PIN, prompt the customer for a PIN without creating a new transaction. If the card reader doesn’t support Online PIN, You need to try again by inserting your card and entering a PIN.`,
|
||||
pickup_card: `The customer can’t use this card to make this payment (it’s possible it was reported lost or stolen). They need to contact your card-issuer for more information.`,
|
||||
pin_try_exceeded: `The allowable number of PIN tries was exceeded. The customer must use another card or method of payment.`,
|
||||
processing_error: `An error occurred while processing the card. The payment needs to be attempted again. If it still can’t be processed, try again later.`,
|
||||
reenter_transaction: `The payment couldn’t be processed by the issuer for an unknown reason. The payment needs to be attempted again. If it still can’t be processed, You need to contact your card-issuer.`,
|
||||
restricted_card: `The customer can’t use this card to make this payment (it’s possible it was reported lost or stolen). You need to contact your card-issuer for more information.`,
|
||||
revocation_of_all_authorizations: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
revocation_of_authorization: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
security_violation: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
service_not_allowed: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
stop_payment_order: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
testmode_decline: `A Stripe test card number was used. A genuine card must be used to make a payment.`,
|
||||
transaction_not_allowed: `The card was declined for an unknown reason. You need to contact your card-issuer for more information.`,
|
||||
try_again_later: `The card was declined for an unknown reason. Ask the customer to attempt the payment again. If subsequent payments are declined, You need to contact your card-issuer for more information.`,
|
||||
withdrawal_count_limit_exceeded: `The customer has exceeded the balance or credit limit available on your card. You need to use an alternative payment method.`,
|
||||
}
|
||||
|
||||
// Page elements
|
||||
const dom = {
|
||||
title: document.getElementById('title'),
|
||||
msg: document.getElementById('msg'),
|
||||
itemsTable: document.getElementById('items-table'),
|
||||
tax: document.getElementById('tax'),
|
||||
shipping: document.getElementById('shipping'),
|
||||
processing: document.getElementById('processing'),
|
||||
subtotal: document.getElementById('subtotal'),
|
||||
total: document.getElementById('total'),
|
||||
shippingAddress: document.getElementById('shipping-address'),
|
||||
contact: document.getElementById('contact'),
|
||||
}
|
||||
|
||||
const emptyCart = () => {
|
||||
Object.keys(localStorage).forEach(i => {
|
||||
if (i.substring(0,5)==='cart_')
|
||||
localStorage.removeItem(i)
|
||||
}); recountCart()
|
||||
}
|
||||
|
||||
const getStripeStatus = async (secret) => {
|
||||
|
||||
const succeed = async () => {
|
||||
dom.title.innerText = `Order placed`
|
||||
dom.msg.innerText = `Your order has been placed and your payment was processed. You may 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.`
|
||||
emptyCart()
|
||||
}
|
||||
const decline = (err_code) => {
|
||||
dom.title.innerText = 'Card declined'
|
||||
dom.msg.innerText = `Your payment was declined. Our payment processor, Stripe, sent back this error: <code>${err_code}</code>. Here's what they think that means: </p>
|
||||
<blockquote>${DECLINE_CODES[err_code]||DECLINE_CODES['generic_decline']}</blockquote>
|
||||
<p>What you do now is up to you. Maybe you want to <a href="/shop/checkout/stripe/">go back to the checkout page</a> and try again.`
|
||||
}
|
||||
const fail = (err) => {
|
||||
dom.title.innerText = 'Payment failed'
|
||||
dom.msg.innerText = `The payment failed due to an ${err} error.`
|
||||
}
|
||||
const pend = () => {
|
||||
dom.title.innerText = 'Processing!'
|
||||
dom.msg.innerText = `Your payment wasn't processed <i>yet</i>. Wait a few minutes and then try <a href="javascript:window.location.reload(true)">refreshing this page</a>. `
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/error-handling?lang=node#use-stored-information
|
||||
const { paymentIntent } = await stripe.retrievePaymentIntent(secret)
|
||||
//payment_intent.last_payment_error
|
||||
if (paymentIntent.last_payment_error !== null) {
|
||||
if (paymentIntent.last_payment_error.type==='StripeCardError')
|
||||
decline(paymentIntent.last_payment_error.code||'generic_decline')
|
||||
else
|
||||
fail(paymentIntent.last_payment_error.type)
|
||||
} else switch (paymentIntent.status) {
|
||||
case 'succeeded':
|
||||
succeed(); break
|
||||
case 'processing':
|
||||
pend(); break
|
||||
default:
|
||||
fail('unknown'); break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
|
||||
// Check stripe status with qs if possible
|
||||
if (qs.payment_intent_client_secret!=null)
|
||||
getStripeStatus(qs.payment_intent_client_secret)
|
||||
|
||||
// Meanwhile, get the order data from api
|
||||
let res; try {
|
||||
res = await fetch(`${API_DOMAIN}/order/${qs.order}`, {
|
||||
headers: {'Accept':'application/json'},
|
||||
})
|
||||
if (!res.ok) throw res.statusText
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch order from API!\n${err}`)
|
||||
}; let order; try {
|
||||
order = await res.json()
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse JSON:\n${err}`)
|
||||
}
|
||||
|
||||
// Get stripe status if we couldn't use the qs
|
||||
if (order.stripe_secret!==qs.payment_intent_client_secret)
|
||||
getStripeStatus(stripe_secret)
|
||||
|
||||
// Populate the page with the order data
|
||||
dom.tax.innerText = formatUSD(order.taxAmount)
|
||||
dom.shipping.innerText = formatUSD(order.shippingAmount)
|
||||
dom.processing.innerText = formatUSD(order.processing)
|
||||
dom.subtotal.innerText = formatUSD(order.subtotal)
|
||||
dom.total.innerText = formatUSD(order.total)
|
||||
dom.shippingAddress.innerHTML = `${shipping_data.name}<br>${shipping_data.addr1}<br>${(shipping_data.addr2)?shipping_data.addr2+'<br>':''}${shipping_data.city}, ${shipping_data.state} ${shipping_data.zip}`
|
||||
dom.contact.innerHTML = `${contact_data.name}<br>${contact_data.phone}<br>${contact_data.email}`
|
||||
order.items.forEach((item) =>
|
||||
dom.itemsTable.innerHTML += `<tr><td>${item.name}</td>
|
||||
<td style="text-align:right">${formatUSD(parseFloat(item.price))}</td>
|
||||
<td style="width:2rem;text-align:right">${item.qty}</td>
|
||||
<td style="text-align:right">${formatUSD(item.subtotal)}</td></tr>`
|
||||
)
|
||||
|
||||
})()
|
|
@ -153,11 +153,6 @@ table#cart-items {
|
|||
}
|
||||
}
|
||||
|
||||
#checkout-button, #submit {
|
||||
margin: 2em 0 1em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
#totals-container {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ title: Cart
|
|||
<p>Select payment method to calculate processing fees: </p><label><input type="radio" value="XMR" data-bind="checked:payment_method"/> <img src="/assets/img/monero.svg" style="width:1em;height:1em"> XMR Monero</label>
|
||||
<!--<br><label><input type="radio" value="BTC" data-bind="checked:payment_method"/> 🪙 BTC ₿itcoin (lightning)</label>-->
|
||||
<br><label><input type="radio" value="USD" data-bind="checked:payment_method" checked/> 💳 USD Debit/credit</label>
|
||||
<p data-bind="visible:payment_method()==='USD'">(Stripe's <a href="https://stripe.com/pricing">fees</a> and <a href="/policies/">terms</a> apply.)</p>
|
||||
<p data-bind="visible:payment_method()==='USD'">Stripe's <a href="https://stripe.com/pricing">processing fee</a> (2.9% + 30¢) and <a href="/policies/">terms</a> apply.</p>
|
||||
<p data-bind="visible:payment_method()==='XMR'">(Payment processing through our secure monero daemon.)</p>
|
||||
<p data-bind="visible:payment_method()==='BTC'">(Payment processing through our secure bitcoin daemon.)</p>
|
||||
</div>
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Checkout
|
||||
---
|
||||
|
||||
<link rel="stylesheet" href="/assets/shop.css">
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
|
||||
<h1>📃 Stripe Checkout</h1>
|
||||
|
||||
<table class="tal" style="width:100%"><thead>
|
||||
<th>Item</th>
|
||||
<th class="tar">Price</th>
|
||||
<th class="tar">Qty</th>
|
||||
<th class="tar">Sub</th>
|
||||
</thead><tbody id="items-table"></tbody></table>
|
||||
<hr>
|
||||
|
||||
<div class="flex" style="align-items:flex-start">
|
||||
<p id="shipping-container">
|
||||
<b>📦 <span class="ul">Shipping</span></b><br>
|
||||
<span id="shipping-address"></span>
|
||||
</p>
|
||||
<p id="contact-container">
|
||||
<b>📇 <span class="ul">Contact</ul></b><br>
|
||||
<span id="contact"></span>
|
||||
</p>
|
||||
<div id="totals-container"><table><tbody>
|
||||
<tr>
|
||||
<td><b>Subtotal:</b></td>
|
||||
<td class="tar"><b id="subtotal"></b></td>
|
||||
</tr><tr>
|
||||
<td>Tax:</td>
|
||||
<td id="tax" class="tar"></td>
|
||||
</tr><tr>
|
||||
<td>Shipping:</td>
|
||||
<td id="shipping" class="tar"></td>
|
||||
</tr><tr>
|
||||
<td>Processing:</td>
|
||||
<td id="processing" class="tar"></td>
|
||||
</tr><tr style="border-top:.1vh solid">
|
||||
<td><b>Total:</b></td>
|
||||
<td class="tar"><b id="total"></b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<p>To change order details, <a href="#" onclick="cancel()">cancel it and return to your cart</a>. Otherwise, enter your payment information below. </p>
|
||||
<div id="payment-form">
|
||||
<div id="payment-element">Loading payment form...</div>
|
||||
<button id="submit" onclick="submitPayment()">
|
||||
<span id="button-text">💳 Place order<span id="final-amount"></span></span>
|
||||
</button>
|
||||
<div id="payment-message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SUCCESS_URL = '{{env.SITE_DOMAIN}}/shop/order/stripe/'
|
||||
const API_DOMAIN = '{{env.API_DOMAIN}}'
|
||||
const stripe = Stripe('{{env.STRIPE_PUB}}')
|
||||
</script>
|
||||
<script src="/assets/scripts/checkout/stripe.js" integrity="{{'/assets/scripts/checkout/stripe.js'|srintegrity}}" defer></script>
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Order
|
||||
---
|
||||
|
||||
<style>
|
||||
#qr_wrapper {
|
||||
max-width: 80vh;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
max-height: 80vw;
|
||||
padding: calc(1.5vh + 1.5vw);
|
||||
background-color: #FFFFFF;
|
||||
margin: auto;
|
||||
}
|
||||
#qr_wrapper div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<table class="tal" style="width:100%"><thead><b>
|
||||
<th>Item</th>
|
||||
<th class="tar">Price</th>
|
||||
<th class="tar">Qty</th>
|
||||
<th class="tar">Sub</th>
|
||||
</b></thead><tbody data-bind="foreach:items"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td class="tar" data-bind="text:price"></td>
|
||||
<td class="tar" data-bind="text:qty" style="width:2rem"></td>
|
||||
<td class="tar" data-bind="text:subtotal"></td>
|
||||
</tr></tbody></table>
|
||||
<hr>
|
||||
<div class="flex" style="align-items:flex-start">
|
||||
<p id="shipping-container">
|
||||
<b>📦 <span class="ul">Shipping</span></b><br>
|
||||
<span data-bind="text:shipname"></span>
|
||||
<br><span data-bind="text:addr1"></span>
|
||||
<br><span data-bind="text:addr2"></span>
|
||||
<br><span data-bind="text:city"></span>,
|
||||
<span data-bind="text:state"></span>
|
||||
<span data-bind="text:zip"></span>
|
||||
</p>
|
||||
<p id="contact-container">
|
||||
<b>📇 <span class="ul">Contact</ul></b><br>
|
||||
<span data-bind="text:contactname"></span>
|
||||
<br><span data-bind="text:phone"></span>
|
||||
<br><span data-bind="text:email"></span>
|
||||
</p>
|
||||
<div id="totals-container">
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<td>Subtotal:</td>
|
||||
<td class="tar" data-bind="text:subtotal"></td>
|
||||
</tr><tr>
|
||||
<td>Tax:</td>
|
||||
<td class="tar" data-bind="text:tax"></td>
|
||||
</tr><tr>
|
||||
<td>Shipping:</td>
|
||||
<td class="tar" data-bind="text:shipping"></td>
|
||||
</tr><tr data-bind="visible:paymentMethod()=='USD'">
|
||||
<td>Processing:</td>
|
||||
<td class="tar" data-bind="text:processing_pretty"></td>
|
||||
</tr><tr style="border-top:.1vh solid">
|
||||
<td><b>Total:</b></td>
|
||||
<td class="tar"><b data-bind="text:total_pretty"></b></td>
|
||||
</tr><tr data-bind="visible:paymentMethod()=='XMR'">
|
||||
<td><b>Monero:</b></td>
|
||||
<td class="tar"><b data-bind="text:totalxmr_pretty"></b>
|
||||
</td></tr>
|
||||
</tbody></table>
|
||||
<p data-bind="visible:isPaid">
|
||||
Paid: <span data-bind="text:paidDate_pretty"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p data-bind="hidden:isPaid">To change order details, <a href="#" onclick="cancel()">cancel it and return to your cart</a>.</p>
|
||||
<hr>
|
||||
|
||||
<h2><span data-bind="visible:paymentMethod()==='USD'">💳 </span><span data-bind="visible:paymentMethod()==='XMR'"><img src="/assets/img/monero.svg" style="width:1em;height:1em"> </span>Payment</h2>
|
||||
|
||||
{# XMR Payment form #}
|
||||
<div data-bind="hidden:(paymentMethod()!='XMR'||isPaid())">
|
||||
<p>Please send <code data-bind="text:unsubmitted"></code> XMR to this address: </p>
|
||||
<pre><code data-bind="text:xmr_address"></code></pre>
|
||||
<p>You can also click, tap, or scan this qr code:</p>
|
||||
<a data-bind="attr:{href:xmr_uri}"><div id="qr_wrapper"><div id="xmr_qr"></div></div></a>
|
||||
</div>
|
||||
|
||||
{# Stripe payment form #}
|
||||
<div data-bind="hidden:(paymentMethod()!='USD'||isPaid())">
|
||||
<div id="stripe-payment">Loading payment form...</div>
|
||||
<button data-bind="click:submitStripePayment(), text:stripeSubmitButtonText(),disable:isPlacingStripeOrder()" style="margin:2em 0 1em;height:4em"></button>
|
||||
</div>
|
||||
|
||||
<hr data-bind="hidden:(isPaid()||paymentMethod()==='USD')">
|
||||
|
||||
{# Stripe payment status #}
|
||||
<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>
|
||||
|
||||
<script>
|
||||
const stripe = Stripe('{{env.STRIPE_PUB}}')
|
||||
const SUCCESS_URL = '{{env.SITE_DOMAIN}}/shop/order/'
|
||||
const API_DOMAIN = '{{env.API_DOMAIN}}'
|
||||
const MONERO_PRICECHECK_SEC = {{env.MONERO_PRICECHECK_SEC}}
|
||||
const MONERO_CHECKOUT_POLL_SECS = {{env.MONERO_CHECKOUT_POLL_SECS}}
|
||||
</script>
|
||||
<script src="/assets/scripts/lib/qrcode.min.js" integrity="sha256-CxytvzTC2wdC92a5wqFc1DTkbS+uuQ8N2NgxGcuqoDc="></script>
|
||||
<script src="/assets/scripts/lib/knockout-3.5.1.min.js" integrity="sha256-6JV7sYKlBHsHvqCkn9IrEWFLGrmsW4KG/LIln0hljnM="></script>
|
||||
{#<script src="/assets/scripts/lib/socket-io_4-5-4.min.js" integrity="sha256-GKNqkn2sVGULGLkD+Ph3ghngLhOUblgdmz4eSZX3Q1s="></script>#}
|
||||
<script src="/assets/scripts/order.js" integrity="{{'/assets/scripts/order.js'|srintegrity}}" defer></script>
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Monero order
|
||||
---
|
||||
|
||||
<style>
|
||||
#qr_wrapper {
|
||||
max-width: 80vh;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
max-height: 80vw;
|
||||
padding: calc(1.5vh + 1.5vw);
|
||||
background-color: #FFFFFF;
|
||||
margin: auto;
|
||||
}
|
||||
#qr_wrapper div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/shop.css">
|
||||
<h1><img src="/assets/img/monero.svg" style="width:1em;height:1em"> Monero <span data-bind="hidden:isPaid">Checkout</span><span data-bind="visible:isPaid">Order</span></h1>
|
||||
<table class="tal" style="width:100%"><thead>
|
||||
<th>Item</th>
|
||||
<th class="tar">Price</th>
|
||||
<th class="tar">Qty</th>
|
||||
<th class="tar">Sub</th>
|
||||
</thead><tbody data-bind="foreach:items"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td class="tar" data-bind="text:price"></td>
|
||||
<td class="tar" data-bind="text:qty" style="width:2rem"></td>
|
||||
<td class="tar" data-bind="text:subtotal"></td>
|
||||
</tr></tbody></table>
|
||||
<hr>
|
||||
<div class="flex" style="align-items:flex-start">
|
||||
<p id="shipping-container">
|
||||
<b>📦 <span class="ul">Shipping</span></b><br>
|
||||
<span data-bind="text:shipname"></span>
|
||||
<br><span data-bind="text:addr1"></span>
|
||||
<br><span data-bind="text:addr2"></span>
|
||||
<br><span data-bind="text:city"></span>,
|
||||
<span data-bind="text:state"></span>
|
||||
<span data-bind="text:zip"></span>
|
||||
</p>
|
||||
<p id="contact-container">
|
||||
<b>📇 <span class="ul">Contact</ul></b><br>
|
||||
<span data-bind="text:contactname"></span>
|
||||
<br><span data-bind="text:phone"></span>
|
||||
<br><span data-bind="text:email"></span>
|
||||
</p>
|
||||
<div id="totals-container"><table><tbody>
|
||||
<tr>
|
||||
<td><b>Subtotal:</b></td>
|
||||
<td class="tar"><b data-bind="text:subtotal"></b></td>
|
||||
</tr><tr>
|
||||
<td>Tax:</td>
|
||||
<td class="tar" data-bind="text:tax"></td>
|
||||
</tr><tr>
|
||||
<td>Shipping:</td>
|
||||
<td class="tar" data-bind="text:shipping"></td>
|
||||
</tr><tr style="border-top:.1vh solid">
|
||||
<td>Total:</td>
|
||||
<td class="tar" data-bind="text:total"></td>
|
||||
</tr><tr>
|
||||
<td><b>Monero:</b></td>
|
||||
<td class="tar"><b data-bind="text:totalxmr_pretty"></b>
|
||||
</td></tr>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<p data-bind="hidden:isPaid">To change order details, <a href="#" onclick="cancel()">cancel it and return to your cart</a>.</p>
|
||||
<hr>
|
||||
<h2>Payment status</h2>
|
||||
<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>
|
||||
<p data-bind="hidden: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: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: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: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><i data-bind="text:checkingStatus"></i></p>
|
||||
<hr data-bind="hidden:isPaid">
|
||||
<div data-bind="hidden:isPaid">
|
||||
<h2>Monero payment</h2>
|
||||
<p>Please send <code data-bind="text:unsubmitted"></code> XMR to this address: </p>
|
||||
<pre><code data-bind="text:xmr_address"></code></pre>
|
||||
<p>You can also click, tap, or scan this qr code:</p>
|
||||
<a data-bind="attr:{href:xmr_uri}"><div id="qr_wrapper"><div id="xmr_qr"></div></div></a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_DOMAIN = '{{env.API_DOMAIN}}'
|
||||
const MONERO_PRICECHECK_SEC = {{env.MONERO_PRICECHECK_SEC}}
|
||||
const MONERO_CHECKOUT_POLL_SECS = '{{env.MONERO_CHECKOUT_POLL_SECS}}'
|
||||
</script>
|
||||
<script src="/assets/scripts/lib/qrcode.min.js" integrity="sha256-CxytvzTC2wdC92a5wqFc1DTkbS+uuQ8N2NgxGcuqoDc="></script>
|
||||
<script src="/assets/scripts/lib/knockout-3.5.1.min.js" integrity="sha256-6JV7sYKlBHsHvqCkn9IrEWFLGrmsW4KG/LIln0hljnM="></script>
|
||||
<script src="/assets/scripts/lib/socket-io_4-5-4.min.js" integrity="sha256-GKNqkn2sVGULGLkD+Ph3ghngLhOUblgdmz4eSZX3Q1s="></script>
|
||||
<script src="/assets/scripts/order/monero.js" defer></script>
|
|
@ -1,50 +0,0 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<link rel="stylesheet" href="/assets/shop.css">
|
||||
<h1>🧾 <span id="title">Loading...</span></h1>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<p id="msg"></p>
|
||||
<hr>
|
||||
<table class="tal" style="width:100%"><thead>
|
||||
<th>Item</th>
|
||||
<th class="tar">Price</th>
|
||||
<th class="tar">Qty</th>
|
||||
<th class="tar">Sub</th>
|
||||
</thead><tbody id="items-table"></tbody></table>
|
||||
<hr>
|
||||
<div class="flex" style="align-items:flex-start">
|
||||
<p id="shipping-container">
|
||||
<b>📦 <span class="ul">Shipping</span></b><br>
|
||||
<span id="shipping-address"></span>
|
||||
</p>
|
||||
<p id="contact-container">
|
||||
<b>📇 <span class="ul">Contact</ul></b><br>
|
||||
<span id="contact"></span>
|
||||
</p>
|
||||
<div id="totals-container"><table><tbody>
|
||||
<tr>
|
||||
<td><b>Subtotal:</b></td>
|
||||
<td class="tar"><b id="subtotal"></b></td>
|
||||
</tr><tr>
|
||||
<td>Tax:</td>
|
||||
<td id="tax" class="tar"></td>
|
||||
</tr><tr>
|
||||
<td>Shipping:</td>
|
||||
<td id="shipping" class="tar"></td>
|
||||
</tr><tr>
|
||||
<td>Processing:</td>
|
||||
<td id="processing" class="tar"></td>
|
||||
</tr><tr style="border-top:.1vh solid">
|
||||
<td><b>Total:</b></td>
|
||||
<td class="tar"><b id="total"></b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<hr>
|
||||
<p><a href="/shop/">< Return to shop</a></p>
|
||||
|
||||
<script>const stripe=Stripe('{{env.STRIPE_PUB}}')</script>
|
||||
<script src="/assets/scripts/order/stripe.js" integrity="{{'/assets/scripts/order/stripe.js'|srintegrity}}" defer></script>
|
|
@ -3,6 +3,7 @@ require('dotenv').config()
|
|||
const fs = require('fs').promises
|
||||
const stripe = require('stripe')(process.env.STRIPE_SEC)
|
||||
const axios = require('axios')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const DATA_DIR = `${__dirname}/../_src/_data`
|
||||
const ORDERS_DIR = `${__dirname}/../orders`
|
||||
|
@ -15,7 +16,7 @@ const MONERO_PRICE_LEEWAY = Number(process.env.MONERO_PRICE_LEEWAY)
|
|||
const readDataFile = async (path) => {
|
||||
const RETRY_INTERVAL = 2000 // 2 seconds
|
||||
try { return JSON.parse(
|
||||
await fs.readFile(`${path}`,'utf8')
|
||||
await fs.readFile(path,'utf8')
|
||||
) } catch (err) {
|
||||
console.log(`Failed to read data from ${path}\n Retrying in ${RETRY_INTERVAL/1000} seconds.`)
|
||||
if (err.code==='ENOENT')
|
||||
|
@ -145,6 +146,7 @@ module.exports = async (req, res) => {
|
|||
}
|
||||
|
||||
const getProcessedOrder = () => new Promise(async (resolve, reject) => {
|
||||
order.key = crypto.randomBytes(64).toString('base64')
|
||||
|
||||
// Process stripe order
|
||||
if (order.paymentMethod==='USD') {
|
||||
|
@ -152,13 +154,14 @@ module.exports = async (req, res) => {
|
|||
// Get stripe payment intent
|
||||
let paymentIntent; try {
|
||||
paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: Math.round(total * 100), // Stripe takes pennies as an integer
|
||||
amount: Math.round(total * 100), // Stripe takes pennies
|
||||
currency: 'usd',
|
||||
payment_method_types: ['card'],
|
||||
description: `San Luis Valley IT order ${order.id}`,
|
||||
statement_descriptor: `slvit.us ${order.id}`, // 22 chars max
|
||||
metadata: {
|
||||
id: order.id,
|
||||
key: order.key,
|
||||
},
|
||||
shipping: {
|
||||
name: order.shippingAddress.name,
|
||||
|
@ -172,6 +175,7 @@ module.exports = async (req, res) => {
|
|||
},
|
||||
}
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
// https://stripe.com/docs/error-handling?lang=node#error-types
|
||||
switch (err.type) {
|
||||
|
@ -196,7 +200,9 @@ module.exports = async (req, res) => {
|
|||
default:
|
||||
res.status(500).send(`Something went wrong and we aren't quite sure what... Could you let us know you got this error by emailing ${process.env.ADMIN_EMAIL||'us'}`)
|
||||
}
|
||||
reject(`[${ip}] failed to create paymentIntent for order ${order.id} due to ${err.type} ${err.code} ${err.message}`)
|
||||
if (err.type==='StripeConnectionError') res.status(504)
|
||||
.send(`Failed to connect to stripe payment processing (on our end). Try again later and let us know if you continue to get this error`)
|
||||
reject(`[${ip}] failed to create paymentIntent for order ${order.id} due to\n ${err.type}: ${err.code} ${err.message}`)
|
||||
}
|
||||
console.log(`[${ip}] paymentIntent created for ${order.id}`)
|
||||
order.stripe_secret = paymentIntent.client_secret
|
||||
|
@ -252,7 +258,7 @@ module.exports = async (req, res) => {
|
|||
res.status(200)
|
||||
.header('Content-Type','application/json')
|
||||
.send(JSON.stringify(processedOrder))
|
||||
console.log(`[${ip}] sent order back to client`)
|
||||
console.log(`[${ip}] sent order ${order.id} back to client`)
|
||||
|
||||
// Save order to fs
|
||||
console.log(`[${ip}] Order ${processedOrder.id} for ${formatUSD(processedOrder.total)} verified.`)
|
||||
|
@ -261,6 +267,7 @@ module.exports = async (req, res) => {
|
|||
await fs.writeFile(orderFile,
|
||||
JSON.stringify({
|
||||
id: processedOrder.id,
|
||||
key: processedOrder.key,
|
||||
orderDate: processedOrder.created,
|
||||
items: processedOrder.items,
|
||||
contact: processedOrder.contact,
|
||||
|
|
|
@ -7,12 +7,19 @@ module.exports = async (req, res) => {
|
|||
const ip = req.ip.slice(7)
|
||||
const orderFile = `${ORDERS_DIR}/${req.params.orderid}.json`
|
||||
let order; try {
|
||||
order = await fs.readFile(orderFile)
|
||||
order = await JSON.parse(await fs.readFile(orderFile))
|
||||
} catch (err) {
|
||||
console.error(`[${ip}] requested ${orderFile} but it couldn't be read.\n${err}`)
|
||||
console.error(`[${ip}] requested ${orderFile} but it couldn't be read:\n${err}`)
|
||||
return res.status(500).send('Failed to read order from disk')
|
||||
}
|
||||
console.log(`[${ip}] Got order ${req.params.orderid}`)
|
||||
return res.header('Content-Type','application/json')
|
||||
.status(200).send(order)
|
||||
if (req.query.key.replace(/ /g,'+')===order.key) {
|
||||
console.log(`[${ip}] Got order ${order.id}`)
|
||||
return res.header('Content-Type','application/json')
|
||||
.status(200).send(order)
|
||||
}
|
||||
else {
|
||||
console.log(`[${ip}] sent wrong key:\n ${req.query.key.replace(/ /g,'+')}`)
|
||||
console.log(`[${ip}] correct key:\n ${order.key}`)
|
||||
return res.status(403).send('Incorrect key!')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}\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_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`,
|
||||
})
|
||||
console.log(`Sent email ${customer_mail_res.messageId} to customer for order ${order.id}`)
|
||||
} catch (err) { console.error(err) }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict'
|
||||
require('dotenv').config()
|
||||
const formatUSD = (v) => v.toLocaleString(undefined, {
|
||||
style: 'currency', currency: 'USD' })
|
||||
const fs = require('fs').promises
|
||||
const axios = require('axios')
|
||||
|
||||
|
@ -30,7 +32,7 @@ const xmrPaidCheck = async (order) => {
|
|||
}
|
||||
|
||||
// Throw hook if paid
|
||||
if (res.amount.expected<=res.amount.covered.unlocked && order.paidDate==null) {
|
||||
if (res.amount!=null) if (res.amount.expected<=res.amount.covered.unlocked && order.paidDate==null) {
|
||||
console.log(`Order ${order.id} has been paid with ${res.amount.expected/1000000000000} XMR`)
|
||||
|
||||
// Save payment data to order
|
||||
|
@ -67,5 +69,5 @@ let interval
|
|||
module.exports = (order) => {
|
||||
if (order.paymentMethod!=='XMR')
|
||||
return console.log(`Invalid paymentMethod: ${order.paymentMethod}`)
|
||||
interval = setInterval(()=>xmrPaidCheck(order), process.env.MONERO_PRICECHECK_SEC)
|
||||
interval = setInterval(()=>xmrPaidCheck(order), 1000*process.env.MONERO_PRICECHECK_SEC)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ const app = express()
|
|||
const ORDERS_DIR = `${__dirname}/../orders`
|
||||
|
||||
// Start webhook forwarder so we don't need a public endpoint
|
||||
require('../lib/run.js')(`stripe listen --api-key '${process.env.STRIPE_RES}' --forward-to 'http://localhost:${process.env.HOOK_PORT}/' --format 'JSON'`)
|
||||
require('../lib/run.js')(`stripe listen --api-key '${process.env.STRIPE_RES}' --forward-to 'http://localhost:${process.env.STRIPE_LISTENER_PORT}/' --format 'JSON'`)
|
||||
|
||||
// Receive that webhook
|
||||
app.listen(process.env.STRIPE_LISTENER_PORT)
|
||||
|
|
Loading…
Reference in New Issue