✨ update flywire
This commit is contained in:
parent
f899760a86
commit
9095b4d9b1
@ -173,7 +173,8 @@ body #root .dashboard-layout{
|
||||
|
||||
.dashboard-menu-container {
|
||||
padding: var(--dashboard-layout-padding, 15px);
|
||||
|
||||
position: sticky;
|
||||
top:var(--dashboard-header-height, 50px);
|
||||
.nav-item {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
|
@ -130,4 +130,14 @@ export const IconBillType = ({style}: IconProps) => {
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const IconLoading = ({size}:{size?:string|number})=>(<svg xmlns="http://www.w3.org/2000/svg" style={{
|
||||
margin: 'auto', display: 'block',...(size?{fontSize:size}:{})
|
||||
}} width="1em" height="1em" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="50" cy="50" r="30" stroke="#6a6a6a" strokeWidth="6" fill="none"></circle>
|
||||
<circle cx="50" cy="50" r="30" stroke="#aaaaaa" strokeWidth="6" strokeLinecap="round" fill="none">
|
||||
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
|
||||
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1"></animate>
|
||||
</circle>
|
||||
</svg>)
|
@ -87,6 +87,8 @@
|
||||
"bill_error": "Bills to be paid do not exist or are overdue",
|
||||
"charge": "Charge",
|
||||
"confirm_pay": "CONFIRM PAYMENT",
|
||||
"query_pay_status": "Check payment status...",
|
||||
"returnBack": "Return Payment System",
|
||||
"text_canceled": "Payment has been cancelled",
|
||||
"text_failed": "Payment Failure",
|
||||
"text_success": "Success",
|
||||
|
@ -87,6 +87,8 @@
|
||||
"bill_error": "待支付账单不存在或已过期",
|
||||
"charge": "手续费",
|
||||
"confirm_pay": "确认支付",
|
||||
"query_pay_status": "查询支付状态...",
|
||||
"returnBack": "返回Payment System",
|
||||
"text_canceled": "支付已取消",
|
||||
"text_failed": "支付失败",
|
||||
"text_success": "支付成功",
|
||||
|
@ -87,6 +87,8 @@
|
||||
"bill_error": "待支付帳單不存在或已過期",
|
||||
"charge": "手續費",
|
||||
"confirm_pay": "確認付款",
|
||||
"query_pay_status": "查詢支付狀態...",
|
||||
"returnBack": "返回Payment System",
|
||||
"text_canceled": "付款已取消",
|
||||
"text_failed": "付款失敗",
|
||||
"text_success": "付款成功",
|
||||
|
@ -12,6 +12,13 @@ import {BillStatus} from "@/service/types.ts";
|
||||
import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
|
||||
|
||||
const DownloadButton = ({bill,text}: { bill: BillModel;text:string }) => {
|
||||
const {loading: downloading, downloadPDF} = useDownloadReceiptPDF()
|
||||
return (<Button
|
||||
onClick={() => downloadPDF(bill)} size={'small'} theme={'solid'}
|
||||
type={'primary'} loading={downloading}>{text}</Button>)
|
||||
}
|
||||
const BillQuery = () => {
|
||||
const {user} = useAuth();
|
||||
const [showBill, setShowBill] = useState<BillModel>()
|
||||
@ -31,7 +38,6 @@ const BillQuery = () => {
|
||||
});
|
||||
}
|
||||
})
|
||||
const {loading: downloading, downloadPDF} = useDownloadReceiptPDF()
|
||||
const {t} = useTranslation()
|
||||
|
||||
const onConfirmCancel = (bill: BillModel) => {
|
||||
@ -56,10 +62,7 @@ const BillQuery = () => {
|
||||
{AppMode == 'development' && <a href={`/pay?bill=${bill.id}`} target={'_blank'}>支付</a>}
|
||||
</>}
|
||||
{
|
||||
bill.status == BillStatus.PAID
|
||||
&& <Button
|
||||
onClick={() => downloadPDF(bill)} size={'small'} theme={'solid'}
|
||||
type={'primary'} loading={downloading}>{t('bill.download_receipt')}</Button>
|
||||
bill.status == BillStatus.PAID && <DownloadButton bill={bill} text={t('bill.download_receipt')}/>
|
||||
}
|
||||
</Space>)
|
||||
}
|
||||
|
@ -1,43 +1,32 @@
|
||||
import styles from "@/pages/pay/pay.module.less";
|
||||
import React, {useMemo} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {getAppUrl} from "@/hooks/useAppUrl.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {IconLoading} from "@/components/icons";
|
||||
import {getFlywirePayUrl} from "@/service/api/bill.ts";
|
||||
|
||||
// 支付费用配置
|
||||
const PayFeeTypeConfig: {
|
||||
[key: string]: string
|
||||
} = {
|
||||
'CAUTION FEE': 'items[caution_fee]', // caution_fee
|
||||
'STUDENT UNION FEE': 'items[student_union_fee]', // student_union_fee
|
||||
'TUITION FEE': 'items[tuition_fee]', // tuition_fee
|
||||
'CREDIT POINT': 'items[credit_point]', // credit_point
|
||||
'APPLICATION FEE': 'items[application_fee]', // application_fee
|
||||
type StartFlyWireProps = {
|
||||
open?: boolean;
|
||||
bill: BillModel
|
||||
}
|
||||
|
||||
function getPaymentFees(bill: BillModel) {
|
||||
// 根据bill details的账单类型设置支付费用
|
||||
return bill.details.map(it => {
|
||||
return (PayFeeTypeConfig[it.bill_type] ?? 'items[other_fee]') + `=${Number(it.amount) * 100}`
|
||||
})
|
||||
}
|
||||
|
||||
export const StartFlyWire: React.FC<{ bill: BillModel }> = ({bill}) => {
|
||||
const callbackUrl = getAppUrl() + "/pay/success?from=flywire";
|
||||
export const StartFlyWire: React.FC<StartFlyWireProps> = ({bill, open}) => {
|
||||
const callbackUrl = getAppUrl() + "/pay/success?from=FLYWIRE&bill=" + bill.id;
|
||||
const {t} = useTranslation()
|
||||
|
||||
const redirectUrl = useMemo(() => {
|
||||
const fees = getPaymentFees(bill) // 支付费用
|
||||
// flywire 支付跳转连接
|
||||
return `${AppConfig.FIY_WIRE_GATEWAY}?provider=HAI&payment_destination=chuhaicollege`
|
||||
+ `&${fees.join('&')}`
|
||||
+ `&student_last_name=${encodeURIComponent(bill.student_english_name)}`
|
||||
+ `&student_id=${encodeURIComponent(bill.student_number || bill.application_number || '')}`
|
||||
+ `&student_email=${encodeURIComponent(bill.student_email)}`
|
||||
+ `&callback_url=${encodeURIComponent(callbackUrl)}`
|
||||
+ `&callback_id=${encodeURIComponent(bill.merchant_ref || '')}`
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [redirectUrl, setRedirectUrl] = useState<string>()
|
||||
useEffect(() => {
|
||||
if (bill) {
|
||||
setLoading(true)
|
||||
getFlywirePayUrl(bill.id, callbackUrl, t(`pay.returnBack`)).then(res => {
|
||||
setRedirectUrl(res.url)
|
||||
}).catch(console.error).finally(() => setLoading(false))
|
||||
}
|
||||
}, [bill])
|
||||
|
||||
return (<a href={redirectUrl} className={styles.btnConfirm}>
|
||||
{t('pay.confirm_pay')}
|
||||
</a>)
|
||||
return (<>
|
||||
{open ? (!loading ? <a href={redirectUrl} className={styles.btnConfirm}>
|
||||
{t('pay.confirm_pay')}
|
||||
</a> : <button className={styles.btnConfirm} style={{fontSize: 26}}><IconLoading/></button>):<></>}
|
||||
</>)
|
||||
}
|
@ -93,7 +93,8 @@ const PayIndex = () => {
|
||||
<div className={styles.payConfirm}>
|
||||
<div className="student-email">Your Email: {bill.student_email}</div>
|
||||
<div className="pay-submit">
|
||||
{ payChannel == 'asia_pay' ? <StartAsiaPay bill={bill} /> : <StartFlyWire bill={bill} />}
|
||||
{ payChannel == 'asia_pay' && <StartAsiaPay bill={bill} />}
|
||||
<StartFlyWire bill={bill} open={payChannel == 'flywire'} />
|
||||
</div>
|
||||
</div>
|
||||
</> : <></>}
|
||||
|
@ -9,7 +9,14 @@
|
||||
width: 600px;
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
.payLoading{
|
||||
padding: 50px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.loadingText{
|
||||
font-size: 32px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.payAmount {
|
||||
color: #fff;
|
||||
background-color: #50ADA7;
|
||||
|
@ -9,11 +9,14 @@ import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
|
||||
import {useEffect} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import {FailIcon} from "@/assets/images/pay/fail.tsx";
|
||||
import {updateBillPaymentSuccess} from "@/service/api/bill.ts";
|
||||
|
||||
type PayResult = 'fail' | 'success' | 'cancel' | 'error' | string;
|
||||
import {getBillDetail, updateBillPaymentSuccess} from "@/service/api/bill.ts";
|
||||
import {BillStatus} from "@/service/types.ts";
|
||||
import {IconLoading} from "@/components/icons";
|
||||
|
||||
type PayResult = 'fail' | 'success' | 'cancel' | 'error' | 'loading' | string;
|
||||
|
||||
const QUERY_MAX_COUNT = 50,
|
||||
QUERY_DELAY = 6000;
|
||||
|
||||
const PayIndex = () => {
|
||||
const {t} = useTranslation()
|
||||
@ -24,14 +27,48 @@ const PayIndex = () => {
|
||||
id?: string;
|
||||
bill?: BillModel
|
||||
status?: string | null;
|
||||
}>({});
|
||||
loading?: boolean;
|
||||
queryCount: number;
|
||||
}>({
|
||||
result: 'loading',
|
||||
queryCount: 0
|
||||
});
|
||||
|
||||
const param = useParams<{ result: PayResult }>();
|
||||
const [search] = useSearchParams(); // 参数有: from: asia_spay || flywire
|
||||
const updateBillSuccess = (billId: number, type: string, ref: string) =>{
|
||||
const updateBillSuccess = (billId: number, type: string, ref: string) => {
|
||||
setState({result: 'loading'})
|
||||
updateBillPaymentSuccess(billId, ref, type).then(bill => {
|
||||
setState({bill})
|
||||
}).catch(()=>{
|
||||
setState({result:'fail'})
|
||||
setState({
|
||||
result: 'success',
|
||||
bill: bill
|
||||
})
|
||||
}).catch(() => {
|
||||
setState({result: 'fail'})
|
||||
})
|
||||
}
|
||||
const checkBillSuccess = (billId: number) => {
|
||||
setState({
|
||||
result: 'loading',
|
||||
queryCount: state.queryCount + 1
|
||||
})
|
||||
|
||||
getBillDetail(Number(billId)).then((bill) => {
|
||||
// 判断bill状态
|
||||
if (bill.status != BillStatus.PAID) {
|
||||
if(state.queryCount > QUERY_MAX_COUNT) {
|
||||
setState({result: 'fail'})
|
||||
}else{
|
||||
// 重试
|
||||
setTimeout(()=>checkBillSuccess(billId), QUERY_DELAY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
result: 'success',
|
||||
bill: bill
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
@ -47,8 +84,13 @@ const PayIndex = () => {
|
||||
return;
|
||||
}
|
||||
if (result == 'success') {
|
||||
updateBillSuccess(Number(bill), from , (from == 'ASIAPAY' ? search.get('Ref')! : search.get('callback_id')!));
|
||||
if (from == 'ASIAPAY') {
|
||||
updateBillSuccess(Number(bill), from, search.get('Ref')!);
|
||||
} else if (from == 'FLYWIRE') {
|
||||
checkBillSuccess(Number(bill));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
setState({result, status})
|
||||
}, []);
|
||||
@ -56,33 +98,42 @@ const PayIndex = () => {
|
||||
return (<div className={styles.container}>
|
||||
<PayLogo/>
|
||||
{state.result == 'success' ? <>
|
||||
<div className={styles.paySuccess}>
|
||||
<div className={styles.successIcon}>
|
||||
<SuccessIcon/>
|
||||
<div className={styles.paySuccess}>
|
||||
<div className={styles.successIcon}>
|
||||
<SuccessIcon/>
|
||||
</div>
|
||||
<h2 className={`${styles.payText} pay-text`}>
|
||||
{t('pay.text_success')}
|
||||
</h2>
|
||||
</div>
|
||||
<h2 className={`${styles.payText} pay-text`}>
|
||||
{t('pay.text_success')}
|
||||
</h2>
|
||||
</div>
|
||||
{state.bill && <div className={styles.payConfirm}>
|
||||
<div className="student-email">{t('bill.bill_number')}: {state.bill.student_number || state.bill.application_number}</div>
|
||||
<div className="pay-submit">
|
||||
<Button
|
||||
theme={'solid'}
|
||||
onClick={() => downloadPDF(state.bill!)}
|
||||
style={{width: 250, marginTop: 20}}>{t('bill.download_receipt')}</Button>
|
||||
</div>
|
||||
</div>}
|
||||
</> : <div className={styles.paySuccess}>
|
||||
<div className={styles.successIcon} style={{color: '#f05672'}}>
|
||||
<FailIcon/>
|
||||
</div>
|
||||
<h2 className={`${styles.payText} pay-text`}>
|
||||
{state.result == 'fail' ? t('pay.text_failed') : (
|
||||
state.result == 'cancel' ? t('pay.text_canceled') : (t('pay.bill_error'))
|
||||
)}
|
||||
</h2>
|
||||
</div>}
|
||||
{state.bill && <div className={styles.payConfirm}>
|
||||
<div
|
||||
className="student-email">{t('bill.bill_number')}: {state.bill.student_number || state.bill.application_number}</div>
|
||||
<div className="pay-submit">
|
||||
<Button
|
||||
theme={'solid'}
|
||||
onClick={() => downloadPDF(state.bill!)}
|
||||
style={{width: 250, marginTop: 20}}>{t('bill.download_receipt')}</Button>
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
: (
|
||||
state.result == 'loading' ? <div className={styles.payLoading}>
|
||||
<IconLoading size={70} />
|
||||
<div className={styles.loadingText}>{t('pay.query_pay_status')}</div>
|
||||
</div>
|
||||
: (<div className={styles.paySuccess}>
|
||||
<div className={styles.successIcon} style={{color: '#f05672'}}>
|
||||
<FailIcon/>
|
||||
</div>
|
||||
<h2 className={`${styles.payText} pay-text`}>
|
||||
{state.result == 'fail' ? t('pay.text_failed') : (
|
||||
state.result == 'cancel' ? t('pay.text_canceled') : (t('pay.bill_error'))
|
||||
)}
|
||||
</h2>
|
||||
</div>)
|
||||
)
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
export default PayIndex;
|
@ -42,6 +42,10 @@ export function getAsiaPayData(id: number) {
|
||||
return get<AsiaPayModel>(`/bills/${id}/asiapay`)
|
||||
}
|
||||
|
||||
export function getFlywirePayUrl(id: number, return_cta: string, return_cta_name: string) {
|
||||
return post<{ url: string }>(`/bills/${id}/flywire`, {return_cta, return_cta_name})
|
||||
}
|
||||
|
||||
// 作废订单
|
||||
export function cancelBill(id: number) {
|
||||
return put(`/bills/${id}/cancel`)
|
||||
@ -51,6 +55,6 @@ export function confirmBills(bill_ids: number[]) {
|
||||
return post(`/bills/apply`, {bill_ids})
|
||||
}
|
||||
|
||||
export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string,payment_channel:string) {
|
||||
return post<BillModel>(`/bills/finish`, {merchant_ref,payment_channel,bill_id})
|
||||
export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string, payment_channel: string) {
|
||||
return post<BillModel>(`/bills/finish`, {merchant_ref, payment_channel, bill_id})
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export function GeneratePdf(bill: BillModel) {
|
||||
title: 'Programme:',
|
||||
content: bill.programme_english_name
|
||||
}, 56)
|
||||
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode}, 70)
|
||||
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode}, bill.programme_english_name.length > 70?70:64)
|
||||
// draw table
|
||||
autoTable(doc, {
|
||||
startY: 80,
|
||||
|
Loading…
x
Reference in New Issue
Block a user