From e3915bb30e01c1b1ec4c4bb27aae9dfc007c74f6 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 6 Feb 2025 18:59:41 -0500 Subject: [PATCH] initial implementation --- index.html | 1 + package.json | 1 + src/purchases/CheckoutTab.ts | 135 +++++++++++++++++++++++++++++--- src/purchases/ConsumablesTab.ts | 10 +-- 4 files changed, 132 insertions(+), 15 deletions(-) diff --git a/index.html b/index.html index d32e95dc1..4c3c2420f 100644 --- a/index.html +++ b/index.html @@ -4909,6 +4909,7 @@

Welcome to the PseudoShop!

+
diff --git a/package.json b/package.json index 2fddc41ac..c13b4293c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "main": "dist/bundle.js", "dependencies": { + "@paypal/paypal-js": "^8.2.0", "@ungap/custom-elements": "^1.3.0", "break_infinity.js": "^2.0.0", "clipboard": "^2.0.11", diff --git a/src/purchases/CheckoutTab.ts b/src/purchases/CheckoutTab.ts index a25c1e9b5..1751fc02b 100644 --- a/src/purchases/CheckoutTab.ts +++ b/src/purchases/CheckoutTab.ts @@ -1,3 +1,4 @@ +import { loadScript } from '@paypal/paypal-js' import { prod } from '../Config' import { changeSubTab, Tabs } from '../Tabs' import { Alert, Notification } from '../UpdateHTML' @@ -33,19 +34,20 @@ export const initializeCheckoutTab = memoize(() => { itemList.insertAdjacentHTML( 'afterend', products.map((product) => (` -
- -
- `)).join('') +
+ +
+ `)).join('') ) - checkout?.addEventListener('click', () => { + checkout?.addEventListener('click', (e) => { if (!tosAgreed) { + e.preventDefault() Notification('You must accept the terms of service first!') return } @@ -79,6 +81,8 @@ export const initializeCheckoutTab = memoize(() => { checkout.removeAttribute('disabled') }) }) + + initializePayPal() }) function addItem (e: MouseEvent) { @@ -156,3 +160,114 @@ export const clearCheckoutTab = () => { const updateTotalPriceInCart = () => { totalCost!.textContent = `${formatter.format(getPrice() / 100)} USD` } + +async function initializePayPal () { + try { + const paypal = await loadScript({ + clientId: 'AYaEpUZfchj2DRdTZJm0ukzxyXGQIHorqy3q1axPQ8RCpiRqkYqg23NiRRYtHptYBRBAyCTL28yEwtb9', + enableFunding: ['venmo'], + disableFunding: ['paylater', 'credit', 'card'] + }) + + paypal?.Buttons?.({ + style: { + shape: 'rect', + layout: 'vertical', + color: 'gold', + label: 'paypal' + }, + + async createOrder () { + try { + const response = await fetch('/api/orders', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: 'YOUR_PRODUCT_ID', + quantity: 'YOUR_PRODUCT_QUANTITY' + } + ] + }) + }) + + const orderData = await response.json() + + if (orderData.id) { + return orderData.id + } + const errorDetail = orderData?.details?.[0] + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData) + + throw new Error(errorMessage) + } catch (error) { + console.error(error) + } + }, + + async onApprove (data, actions) { + try { + const response = await fetch( + `/api/orders/${data.orderID}/capture`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ) + + const orderData = await response.json() + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0] + + if (errorDetail?.issue === 'INSTRUMENT_DECLINED') { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per + // https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart() + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error( + `${errorDetail.description} (${orderData.debug_id})` + ) + } else if (!orderData.purchase_units) { + throw new Error(JSON.stringify(orderData)) + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + const transaction = orderData?.purchase_units?.[0]?.payments + ?.captures?.[0] + || orderData?.purchase_units?.[0]?.payments + ?.authorizations?.[0] + + Notification( + `Transaction ${transaction.status}: ${transaction.id}. See console for all available details` + ) + console.log( + 'Capture result', + orderData, + JSON.stringify(orderData, null, 2) + ) + } + } catch (error) { + console.error(error) + Notification( + `Sorry, your transaction could not be processed... ${error}` + ) + } + } + }).render('#checkout-paypal') + } catch {} +} diff --git a/src/purchases/ConsumablesTab.ts b/src/purchases/ConsumablesTab.ts index 52230170a..d1f0f3c8a 100644 --- a/src/purchases/ConsumablesTab.ts +++ b/src/purchases/ConsumablesTab.ts @@ -34,11 +34,11 @@ const initializeConsumablesTab = memoize(() => { const alert = await Confirm(`Please confirm you would like to activate a ${key} for 500 PseudoCoins`) if (!alert) return Alert('Purchase cancelled') else { - sendToWebsocket(JSON.stringify({ - type: 'consume', - consumable: key - })) - } + sendToWebsocket(JSON.stringify({ + type: 'consume', + consumable: key + })) + } }) }) })