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