feat: basic order handling

This commit is contained in:
Vlad Stan 2023-07-10 14:54:05 +03:00
parent e97a327aec
commit 6a093c8890
4 changed files with 222 additions and 11 deletions

View file

@ -117,12 +117,8 @@
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn @click="placeOrder()" flat color="primary">
<q-btn @click="requestInvoice(cart)" flat color="primary">
Place Order Place Order
</q-btn> </q-btn>
</q-card-actions> </q-card-actions>

View file

@ -5,7 +5,7 @@ async function shoppingCartCheckout(path) {
name: 'shopping-cart-checkout', name: 'shopping-cart-checkout',
template, template,
props: ['cart', 'stall'], props: ['cart', 'stall', 'customer-pubkey'],
data: function () { data: function () {
return { return {
paymentMethod: 'ln', paymentMethod: 'ln',
@ -58,9 +58,53 @@ async function shoppingCartCheckout(path) {
selectShippingZone: function (zone) { selectShippingZone: function (zone) {
this.shippingZone = zone this.shippingZone = zone
}, },
requestInvoice: function () {
} async placeOrder() {
console.log('### placeOrder cart', this.cart)
console.log('### placeOrder stal', this.stall)
if (!this.shippingZone) {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: 'Please select a shipping zone!',
})
return
}
if (!this.customerPubkey) {
this.$emit('login-required')
return
}
const order = {
address: this.contactData.address,
message: this.contactData.message,
contact: {
nostr: this.customerPubkey,
email: this.contactData.email
},
items: Array.from(this.cart.products, p => {
return { product_id: p.id, quantity: p.orderedQuantity }
}),
shipping_id: this.shippingZone.id,
type: 0
}
const created_at = Math.floor(Date.now() / 1000)
order.id = await hash(
[this.customerPubkey, created_at, JSON.stringify(order)].join(':')
)
const event = {
...(await NostrTools.getBlankEvent()),
kind: 4,
created_at,
tags: [['p', this.stall.pubkey]],
pubkey: this.customerPubkey
}
this.$emit('place-order', { event, order })
},
}, },
created() { created() {
console.log('### shoppingCartCheckout', this.stall) console.log('### shoppingCartCheckout', this.stall)

View file

@ -57,6 +57,15 @@ const market = async () => {
checkoutStall: null, checkoutStall: null,
activePage: 'market', activePage: 'market',
activeOrderId: null,
qrCodeDialog: {
data: {
payment_request: null
},
dismissMsg: null,
show: false
},
searchNostr: false, searchNostr: false,
drawer: true, drawer: true,
@ -87,6 +96,7 @@ const market = async () => {
} }
}, },
naddr: null, naddr: null,
loading: false
} }
}, },
computed: { computed: {
@ -137,8 +147,10 @@ const market = async () => {
this.merchants = this.$q.localStorage.getItem('nostrmarket.merchants') || [] this.merchants = this.$q.localStorage.getItem('nostrmarket.merchants') || []
this.shoppingCarts = this.$q.localStorage.getItem('nostrmarket.shoppingCarts') || [] this.shoppingCarts = this.$q.localStorage.getItem('nostrmarket.shoppingCarts') || []
// Check for user stored
this.account = this.$q.localStorage.getItem('nostrmarket.account') || null this.account = this.$q.localStorage.getItem('nostrmarket.account') || null
//console.log("UUID", crypto.randomUUID()) //console.log("UUID", crypto.randomUUID())
// Check for stored merchants and relays on localStorage // Check for stored merchants and relays on localStorage
@ -204,6 +216,9 @@ const market = async () => {
await this.initNostr() await this.initNostr()
this.$q.loading.hide() this.$q.loading.hide()
await this.listenForIncommingDms(this.merchants.map(m => m.publicKey))
}, },
methods: { methods: {
async deleteAccount() { async deleteAccount() {
@ -616,8 +631,138 @@ const market = async () => {
this.checkoutCart = cart this.checkoutCart = cart
this.checkoutStall = this.stalls.find(s => s.id === cart.id) this.checkoutStall = this.stalls.find(s => s.id === cart.id)
this.setActivePage('shopping-cart-checkout') this.setActivePage('shopping-cart-checkout')
},
async placeOrder({ event, order }) {
if (!this.account.privkey) {
this.openAccountDialog()
return
}
this.activeOrderId = order.id
event.content = await NostrTools.nip04.encrypt(
this.account.privkey,
this.checkoutStall.pubkey,
JSON.stringify(order)
)
event.id = NostrTools.getEventHash(event)
event.sig = await NostrTools.signEvent(event, this.account.privkey)
this.sendOrderEvent(event)
},
sendOrderEvent(event) {
const pub = this.pool.publish(Array.from(this.relays), event)
pub.on('ok', () => {
this.qrCodeDialog.show = true
this.$q.notify({
type: 'positive',
message: 'The order has been placed!'
})
})
pub.on('failed', (error) => {
// do not show to user. It is possible that only one relay has failed
console.error(error)
})
},
async listenForIncommingDms(publicKeys) {
if (!this.account?.privkey) {
this.$q.notify({
message: 'Cannot listen for direct messages. You need to login first!',
icon: 'warning'
})
return
}
try {
const filters = [
{
kinds: [4],
authors: publicKeys,
'#p': [this.account.pubkey]
}
]
const subs = this.pool.sub(Array.from(this.relays), filters)
subs.on('event', async event => {
const receiverPubkey = event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]
if (receiverPubkey !== this.account.pubkey) {
console.log('Unexpected DM. Dropped!')
return
}
await this.handleIncommingDm(event)
})
} catch (err) {
console.error(`Error: ${err}`)
}
},
async handleIncommingDm(event) {
try {
const plainText = await NostrTools.nip04.decrypt(
this.account.privkey,
event.pubkey,
event.content
)
console.log('### plainText', plainText)
if (!isJson(plainText)) return
const jsonData = JSON.parse(plainText)
if (jsonData.type === 1) {
this.handlePaymentRequest(jsonData)
} else if (jsonData.type === 2) {
console.log('### this.qrCodeDialog.dismissMsg', this.qrCodeDialog.dismissMs)
console.log('### jsonData', jsonData)
if (this.qrCodeDialog.dismissMsg) {
this.qrCodeDialog.dismissMsg()
this.qrCodeDialog.show = false
}
const message = jsonData.shipped ? 'Order shipped' : jsonData.paid ? 'Order paid' : 'Order updated'
this.$q.notify({
type: 'positive',
message: message
})
}
} catch (e){
console.warn('Unable to handle incomming DM', e)
}
},
handlePaymentRequest(json) {
console.log('### handlePaymentRequest', json, this.activeOrderId)
if (!json.payment_options?.length) {
this.qrCodeDialog.data.message = json.message || 'Unexpected error'
return
}
console.log('### ')
if (json.id && (json.id !== this.activeOrderId)) {
// not for active order, store somewehre else
return
}
let paymentRequest = json.payment_options.find(o => o.type == 'ln')
.link
if (!paymentRequest) return
this.qrCodeDialog.data.payment_request = paymentRequest
this.qrCodeDialog.dismissMsg = this.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
} }
// else if (json.paid) {
// this.closeQrCodeDialog()
// this.$q.notify({
// type: 'positive',
// message: 'Sats received, thanks!',
// icon: 'thumb_up'
// })
// this.activeOrder = null
// return cb()
// } else {
// return
// }
} }
}) })
} }

View file

@ -213,8 +213,9 @@
<shopping-cart-list v-else-if="activePage === 'shopping-cart-list'" :carts="shoppingCarts" <shopping-cart-list v-else-if="activePage === 'shopping-cart-list'" :carts="shoppingCarts"
@add-to-cart="addProductToCart" @remove-from-cart="removeProductFromCart" @remove-cart="removeCart" @add-to-cart="addProductToCart" @remove-from-cart="removeProductFromCart" @remove-cart="removeCart"
@checkout-cart="checkoutStallCart"></shopping-cart-list> @checkout-cart="checkoutStallCart"></shopping-cart-list>
<shopping-cart-checkout v-else-if="activePage === 'shopping-cart-checkout'" <shopping-cart-checkout v-else-if="activePage === 'shopping-cart-checkout'" :cart="checkoutCart"
:cart="checkoutCart" :stall="checkoutStall"></shopping-cart-checkout> :stall="checkoutStall" :customer-pubkey="account?.pubkey" @login-required="openAccountDialog"
@place-order="placeOrder"></shopping-cart-checkout>
<customer-stall v-else-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" <customer-stall v-else-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)"
:products="filterProducts" :stall-products="products.filter(p => p.stall_id == activeStall)" :products="filterProducts" :stall-products="products.filter(p => p.stall_id == activeStall)"
:product-detail="activeProduct" :relays="relays" :account="account" :pool="pool" :styles="config?.opts ?? {}" :product-detail="activeProduct" :relays="relays" :account="account" :pool="pool" :styles="config?.opts ?? {}"
@ -281,6 +282,31 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- END CHECKOUT DIALOG -->
<!-- INVOICE DIALOG -->
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div class="q-my-lg" v-if="qrCodeDialog.data.message">
<p><span v-text="qrCodeDialog.data.message"></span> </p>
</div>
<a v-else :href="'lightning:' + qrCodeDialog.data.payment_request">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode v-if="qrCodeDialog.data.payment_request" :value="qrCodeDialog.data.payment_request"
:options="{width: 340}" class="rounded-borders"></qrcode>
</q-responsive>
</a>
<q-inner-loading :showing="loading">
<q-spinner-puff size="50px" color="primary" />
</q-inner-loading>
</div>
<div class="row q-mt-lg">
<q-btn v-if="qrCodeDialog.data.payment_request" outline color="grey"
@click="copyText(qrCodeDialog.data.payment_request)">Copy invoice</q-btn>
<q-btn flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</q-layout> </q-layout>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script> <script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>