React 嵌入 PayPal 最新寫法

Posted by Young on 2022-12-29
Estimated Reading Time 4 Minutes
Words 967 In Total
Viewed Times

PayPal Developer

PayPal Developer 建立一個 Personal Account & 一個 Business Account,並在 Sandbox 裡面創立一個 Business Account,這樣就可以在 Sandbox 裡面進行測試。

Testing Tools -> Sandbox Accounts -> Create account -> Create custom account
paypal_sandboxAccount

  • Business Account:收款帳號
  • Fake Cutomer Account(Personal Account):等等測試匯款用帳號

personal_business

接下來至 App & Credentials 頁面創建 App,並且記得 Sandbox Account 要選剛剛創建的 Business Account
create_app

創建完後,就可以在 Dashboard 上看到剛剛創建的 App,等等要負責拿來做 API CALL 的就是 Client ID,而 Secret Key 跟大多數 API 邏輯一樣,不應公開。

App_clientID

程式碼撰寫

相較以往舊的寫法直接跟付款頁面寫在一起,我會改用寫成 Component 並傳遞 options 進去,這樣可以讓程式碼更好維護,也可以讓不同的付款頁面使用不同的 options。

建立 Component

建立一個名為 PayPalPayment.js 的 Component:

無限循環更新 BUG

原本若將 dispatch,options 寫進 useEffect 的依賴陣列內,會出現 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.錯誤。

白話文:在 useEffect 裡造成無限循環更新

useEffect 的依賴陣列包含 currency, showSpinner, dispatch, options,而 showSpinner 始終為 false,這不會導致無限循環更新。所以問題應該是出在 options 對象。

這個問題出現是因為 options 對象在每次渲染時引用都會改變,即使其內部屬性值沒有更改。這導致 useEffect 無限次地重新運行,從而導致最大更新深度超出的警告。

1
2
3
4
5
6
7
8
9
10
// PayPalPayment.js
useEffect(() => {
dispatch({
type: "resetOptions",
value: {
...options,
currency: currency,
},
});
}, [currency, showSpinner, dispatch, options]);

解決辦法:

  • 使用JSON.stringify()來比較 options 對象是否有更改。這可以避免在 options 對象的屬性值相同但引用不同時觸發重新渲染。
  • 使用React.useMemo來優化 options 對象的計算。將僅在 currency 或 options 更改時重新計算 updatedOptions 對象。

完整程式碼

最終程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// PayPalPayment.js
import React, { useEffect } from "react";
import {
PayPalScriptProvider,
PayPalButtons,
usePayPalScriptReducer,
} from "@paypal/react-paypal-js";

const ButtonWrapper = ({ currency, amount, onSuccess, showSpinner }) => {
const [{ options, isPending }, dispatch] = usePayPalScriptReducer();

const updatedOptions = React.useMemo(() => {
return {
...options,
currency: currency,
};
}, [currency, options]);

useEffect(() => {
if (JSON.stringify(options) !== JSON.stringify(updatedOptions)) {
dispatch({
type: "resetOptions",
value: updatedOptions,
});
}
}, [currency, showSpinner, dispatch, updatedOptions, options]);

return (
<>
{showSpinner && isPending && <div className="spinner" />}
<PayPalButtons
style={{ layout: "vertical" }}
disabled={false}
forceReRender={[amount, currency]}
fundingSource={undefined}
createOrder={(data, actions) => {
return actions.order
.create({
purchase_units: [
{
amount: {
currency_code: currency,
value: amount,
},
},
],
})
.then((orderId) => {
return orderId;
});
}}
onApprove={(data, actions) => {
return actions.order.capture().then(() => {
onSuccess();
});
}}
/>
</>
);
};

const PayPalPayment = ({ amount, onSuccess }) => {
const currency = "TWD";

return (
<div style={{ maxWidth: "750px", minHeight: "200px" }}>
<PayPalScriptProvider
options={{
"client-id":
"Afy7WAvwiFBbV7ECCDPj842rGP_016i_xz7-CoBIKIlwY1Ss55714aDS4EW8aCYbd2ftDyXuI9yh0R3f",
components: "buttons",
currency: currency,
}}
>
<ButtonWrapper
currency={currency}
amount={amount}
onSuccess={onSuccess}
showSpinner={false}
/>
</PayPalScriptProvider>
</div>
);
};
export default PayPalPayment;

使用 Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// OrderPage.js
...
import PayPalPayment from "../components/PayPalPayment";
.............
const [sdkReady, setSdkReady] = useState(false);

const orderDetails = useSelector((state) => state.orderDetails);
const { order, error, loading } = orderDetails;

const orderPay = useSelector((state) => state.orderPay);
const { loading: loadingPay, success: successPay } = orderPay;
.............
useEffect(() => {
if (!order || successPay || order.id !== Number(orderId)) {
// 沒 RESET 會一直導致跑 <Loader />
dispatch({ type: ORDER_PAY_RESET })
dispatch(getOrderDetails(orderId));
} else if (!order.isPaid) {
setSdkReady(true);
}
}, [dispatch, navigate, order, orderId, successPay]);

const successPaymenyHadler = (paymentResult) => {
console.log(paymentResult);
dispatch(payOrder(orderId, paymentResult));
};
return (
<>
.............
{/* PayPal */}
{!order.isPaid && (
<ListGroup.Item>
{" "}
{loadingPay && <Loader />}
{!sdkReady ? (
<Loader />
) : (
<PayPalPayment
amount={order.totalPrice}
onSuccess={successPaymenyHadler}
/>
)}
</ListGroup.Item>
)}
.............
</>
)

驗證交易結果 Sandbox.paypal

www.sandbox.paypal 用剛剛創的 Business Account 帳號登入,看到剛剛的交易紀錄就代表成功了。

payment_result


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版