From 3cbaebb5d3450920ca5a9750417bf8f9494a5ee5 Mon Sep 17 00:00:00 2001 From: caiyuchao Date: Wed, 23 Apr 2025 16:00:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20paypal=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + package.json | 1 + pnpm-lock.yaml | 15 +++ .../order-confirm/orderConfirmModal.vue | 4 + src/components/payment/paypal-button.vue | 26 +++++ src/service/api/payment.ts | 17 ++++ src/typings/auto-imports.d.ts | 3 + src/typings/components.d.ts | 1 + src/utils/paypal.ts | 99 +++++++++++++++++++ 9 files changed, 167 insertions(+) create mode 100644 src/components/payment/paypal-button.vue create mode 100644 src/utils/paypal.ts diff --git a/.env b/.env index 41182a0..412035b 100644 --- a/.env +++ b/.env @@ -35,3 +35,4 @@ VITE_SERVICE_EXPIRED_TOKEN_CODES=403 VITE_SERVICE_SERVER_ERROR_CODE=500 +VITE_PAYPAL_CLIENT_ID=AfPgwFAmo9K7KCqiiGpNRCyQMSxI6V33eH-nEMnVndJNVEYOEOEn5wSPkHUybfzcjDLnBejt-RKnIfqX diff --git a/package.json b/package.json index 0b07696..f3edd98 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@better-scroll/core": "2.5.1", "@iconify/vue": "4.1.2", + "@paypal/paypal-js": "^8.2.0", "@sa/axios": "workspace:*", "@sa/color-palette": "workspace:*", "@sa/hooks": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41d365a..4377222 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@iconify/vue': specifier: 4.1.2 version: 4.1.2(vue@3.4.27(typescript@5.4.5)) + '@paypal/paypal-js': + specifier: ^8.2.0 + version: 8.2.0 '@sa/axios': specifier: workspace:* version: link:packages/axios @@ -730,6 +733,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@paypal/paypal-js@8.2.0': + resolution: {integrity: sha512-hLg5wNORW3WiyMiRNJOm6cN2IqjPlClpxd971bEdm0LNpbbejQZYtesb0/0arTnySSbGcxg7MxjkZ/N5Z5qBNQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3263,6 +3269,9 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise-polyfill@8.3.0: + resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4633,6 +4642,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@paypal/paypal-js@8.2.0': + dependencies: + promise-polyfill: 8.3.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -7553,6 +7566,8 @@ snapshots: progress@2.0.3: {} + promise-polyfill@8.3.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 diff --git a/src/components/order-confirm/orderConfirmModal.vue b/src/components/order-confirm/orderConfirmModal.vue index 2fab4c4..8312f99 100644 --- a/src/components/order-confirm/orderConfirmModal.vue +++ b/src/components/order-confirm/orderConfirmModal.vue @@ -3,6 +3,7 @@ import { defineProps, defineEmits, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { AlipayOutlined, WechatOutlined, WalletOutlined } from '@ant-design/icons-vue'; import { Modal } from 'ant-design-vue'; +import PaypalButton from '@/components/payment/paypal-button.vue'; const { t } = useI18n(); @@ -128,6 +129,9 @@ const handleCancel = () => { {{ t('page.order.wxpay') }} + diff --git a/src/components/payment/paypal-button.vue b/src/components/payment/paypal-button.vue new file mode 100644 index 0000000..5b2900c --- /dev/null +++ b/src/components/payment/paypal-button.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/service/api/payment.ts b/src/service/api/payment.ts index 8f2ce50..21cfba5 100644 --- a/src/service/api/payment.ts +++ b/src/service/api/payment.ts @@ -37,3 +37,20 @@ export function payBalance(params: { orderId: string }) { } }); } + +/** Paypal createOrder */ +export function payPalOrders(params: {orderId: number}) { + return request({ + url: '/payment/paypal/orders', + method: 'post', + params + }); +} + +/** Paypal captureOrder */ +export function payPalCapture(paypalOrderId: string, orderId: number) { + return request({ + url: `/payment/paypal/orders/${paypalOrderId}/capture/${orderId}`, + method: 'post' + }); +} diff --git a/src/typings/auto-imports.d.ts b/src/typings/auto-imports.d.ts index 4db86d8..7ac17fd 100644 --- a/src/typings/auto-imports.d.ts +++ b/src/typings/auto-imports.d.ts @@ -30,6 +30,7 @@ declare global { const computedEager: typeof import('@vueuse/core')['computedEager'] const computedInject: typeof import('@vueuse/core')['computedInject'] const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] + const constructPaypalBtn: typeof import('../utils/paypal')['constructPaypalBtn'] const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] const controlledRef: typeof import('@vueuse/core')['controlledRef'] const createApp: typeof import('vue')['createApp'] @@ -180,6 +181,8 @@ declare global { const onUpdated: typeof import('vue')['onUpdated'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const payBalance: typeof import('../service/api/payment')['payBalance'] + const payPalCapture: typeof import('../service/api/payment')['payPalCapture'] + const payPalOrders: typeof import('../service/api/payment')['payPalOrders'] const pick: typeof import('lodash-es')['pick'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 383b76f..5791012 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -80,6 +80,7 @@ declare module 'vue' { LookForward: typeof import('./../components/custom/look-forward.vue')['default'] MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default'] OrderConfirmModal: typeof import('./../components/order-confirm/orderConfirmModal.vue')['default'] + PaypalButton: typeof import('./../components/payment/paypal-button.vue')['default'] PinToggler: typeof import('./../components/common/pin-toggler.vue')['default'] ReloadButton: typeof import('./../components/common/reload-button.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/utils/paypal.ts b/src/utils/paypal.ts new file mode 100644 index 0000000..41ae85a --- /dev/null +++ b/src/utils/paypal.ts @@ -0,0 +1,99 @@ +import { loadScript } from "@paypal/paypal-js"; +import { payPalOrders, payPalCapture } from '@/service/api/payment'; + +export const constructPaypalBtn = async(orderId: number, currency: string) =>{ + loadScript({ clientId: import.meta.env.VITE_PAYPAL_CLIENT_ID, disableFunding: ["paylater"], currency }) + .then((paypal: any) => { + paypal + .Buttons({ + style: { + shape: "rect", + layout: "vertical", + color: "white", + label: "paypal", + }, + message: { + + }, + + async createOrder() { + try { + const response = await payPalOrders({orderId: orderId}); + + const orderData = response.data; + 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); + // resultMessage(`Could not initiate PayPal Checkout...

${error}`); + } + }, + + async onApprove(data, actions) { + try { + const response = await payPalCapture(data.orderID, orderId) + + const orderData = response.data + // 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]; + // resultMessage( + // `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); + // resultMessage( + // `Sorry, your transaction could not be processed...

${error}` + //); + } + }, + }) + .render("#paypal-buttons") + .catch((error) => { + console.error("failed to render the PayPal Buttons", error); + }); + }) + .catch((error) => { + console.error("failed to load the PayPal JS SDK script", error); + }); + } + +// Example function to show a result to the user. Your site's UI library can be used instead. +function resultMessage(message: any) { + const container: any = document.querySelector("#result-message"); + container.innerHTML = message; +} \ No newline at end of file