www/_src/assets/scripts/order.js

439 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict'
const formatUSD = (v) => v.toLocaleString(undefined, {
style: 'currency', currency: 'USD' })
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 cant be authorized. Attempt the payment again. If you still cant 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 thats 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 couldnt be reached, so the payment couldnt be authorized. Attempt the payment again. If you still cant 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 isnt 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 doesnt support Online PIN, You need to try again by inserting your card and entering a PIN.`,
pickup_card: `The customer cant use this card to make this payment (its 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 cant be processed, try again later.`,
reenter_transaction: `The payment couldnt be processed by the issuer for an unknown reason. The payment needs to be attempted again. If it still cant be processed, You need to contact your card-issuer.`,
restricted_card: `The customer cant use this card to make this payment (its 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()
}
const cancel = async () => {
let res; try { res = await fetch(
`${API_DOMAIN}/order/${qstr.id}?key=${qstr.key}`,
{ method:'DELETE' }
) } catch (err) { console.error(err) }
finally { window.location = '/shop/cart/' }
}
let xmr_price = 160 // For getting fees in USD before the first price loads
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 * 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()
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.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('')
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(new Date())
self.paidDate_pretty = ko.pureComputed(() =>
self.paidDate().toDateString()
)
self.isPaidUSD = ko.observable(false)
self.isPaidXMR = ko.pureComputed(() =>
( (self.paymentMethod()==='XMR') &&
( self.unlocked() >= self.totalxmr() ) )
)
self.isPaid = ko.pureComputed(() =>
(self.isPaidUSD() || self.isPaidXMR())
)
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%20order%20${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.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.trackingNumbers_newlined = ko.pureComputed(() =>
self.trackingNumbers().join('\n')
)
self.tracking_url = ko.pureComputed(() => {
if (self.shipCarrier()==='USPS')
return `https://tools.usps.com/go/TrackConfirmAction?tLabels=${self.trackingNumbers().join('%2C')}%2C`
else return ''
})
self.isShipped = ko.pureComputed(() =>
(self.shipDate()!=null)
)
;(async () => {
const getStripeStatus = async (secret) => {
// https://stripe.com/docs/error-handling?lang=node#use-stored-information
const { paymentIntent } = await stripe.retrievePaymentIntent(secret)
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(`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 (!self.paidDate()) self.paidDate(new Date())
if (localStorage.getItem('cartOrder')==self.orderId())
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(new Date(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)
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)
self.shipDate( (order.shipping.date==null)?
null:new Date(order.shipping.date) )
self.shipCarrier(order.shipping.carrier)
self.trackingNumbers(order.shipping.tracking)
// USD payment
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)
}
// Create stripe payment form
else {
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)
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) }
})
}
}
// Monero payment
else if (order.paymentMethod==='XMR') {
self.xmr_address(order.xmr_address)
self.totalxmr(order.totalxmr)
// Price checking
const getXmrPrice = async () => {
let res; try {
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)}`)
xmr_price = parsedRes.monero.usd
}; getXmrPrice()
setInterval(getXmrPrice, MONERO_PRICECHECK_SEC*1000)
// Transaction polling
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()
if (!self.isPaid()) setInterval(() => {
if (poll_in<=0) {
self.checkingStatus('Checking for transactions...')
if (!isChecking) getTransactions()
} else {
self.checkingStatus(`Checking for transactions in ${
poll_in--}...`)
}
}, 1000)
else {
if (localStorage.getItem('cartOrder')==self.orderId())
emptyCart()
if (!self.paidDate()) self.paidDate(new Date())
}
// Websockets (to replace polling)
// let 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())