Skip to content

Commit 927e7f8

Browse files
authored
Merge pull request #1115 from topcoder-platform/PM-1374_require-otp-when-withdrawing
PM-1374 - require otp when withdrawing
2 parents 2ab8f98 + a30e16b commit 927e7f8

File tree

9 files changed

+293
-209
lines changed

9 files changed

+293
-209
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/* eslint-disable max-len */
2+
/* eslint-disable react/jsx-no-bind */
3+
import { AxiosError } from 'axios'
4+
import { Link } from 'react-router-dom'
5+
import { toast } from 'react-toastify'
6+
import { FC, useMemo, useState } from 'react'
7+
8+
import { ConfirmModal } from '~/libs/ui'
9+
10+
import { processWinningsPayments } from '../../../lib/services/wallet'
11+
import { WalletDetails } from '../../../lib/models/WalletDetails'
12+
import { Winning } from '../../../lib/models/WinningDetail'
13+
import { nullToZero } from '../../../lib/util'
14+
import { useOtpModal } from '../../../lib/components/otp-modal'
15+
16+
import styles from './Winnings.module.scss'
17+
18+
interface ConfirmPaymentModalProps {
19+
userEmail: string;
20+
payments: Winning[]
21+
walletDetails: WalletDetails
22+
onClose: (done?: boolean) => void
23+
}
24+
25+
const ConfirmPaymentModal: FC<ConfirmPaymentModalProps> = props => {
26+
const [otpModal, collectOtp] = useOtpModal(props.userEmail)
27+
const [isProcessing, setIsProcessing] = useState(false)
28+
29+
const winningIds = useMemo(() => props.payments.map(p => p.id), [props.payments])
30+
const totalAmount = useMemo(() => props.payments.reduce((acc, payment) => acc + parseFloat(payment.grossPayment.replace(/[^0-9.-]+/g, '')), 0), [props.payments])
31+
const taxWithholdAmount = (parseFloat(nullToZero(props.walletDetails.taxWithholdingPercentage ?? '0')) * totalAmount) / 100
32+
const feesAmount = parseFloat(nullToZero(props.walletDetails.estimatedFees ?? '0'))
33+
const netAmount = totalAmount - taxWithholdAmount - feesAmount
34+
35+
const processPayouts = async (otpCode?: string): Promise<void> => {
36+
setIsProcessing(true)
37+
if (!otpCode) {
38+
toast.info('Processing payments...', {
39+
position: toast.POSITION.BOTTOM_RIGHT,
40+
})
41+
}
42+
43+
try {
44+
await processWinningsPayments(winningIds, otpCode)
45+
toast.success('Payments processed successfully!', {
46+
position: toast.POSITION.BOTTOM_RIGHT,
47+
})
48+
props.onClose(true)
49+
} catch (error) {
50+
if ((error as any)?.code?.startsWith('otp_')) {
51+
toast.info((error as any).message)
52+
const code = await collectOtp((error as any)?.message)
53+
if (code) {
54+
processPayouts(code as string)
55+
} else {
56+
setIsProcessing(false)
57+
}
58+
59+
return
60+
}
61+
62+
let message = 'Failed to process payments. Please try again later.'
63+
64+
if (error instanceof AxiosError) {
65+
message = error.response?.data?.error?.message ?? error.response?.data?.message ?? error.message ?? ''
66+
67+
message = message.charAt(0)
68+
.toUpperCase() + message.slice(1)
69+
}
70+
71+
toast.error(message, {
72+
position: toast.POSITION.BOTTOM_RIGHT,
73+
})
74+
}
75+
76+
setIsProcessing(false)
77+
}
78+
79+
return (
80+
<>
81+
<ConfirmModal
82+
size='lg'
83+
maxWidth='610px'
84+
title='Payment Confirmation'
85+
action='Confirm Payment'
86+
onClose={() => props.onClose()}
87+
onConfirm={processPayouts}
88+
isProcessing={isProcessing}
89+
open
90+
>
91+
<div className={`${styles.processing} body-medium-normal`}>
92+
Processing Payment: $
93+
{totalAmount.toFixed(2)}
94+
{' '}
95+
</div>
96+
{props.walletDetails && (
97+
<>
98+
<div className={styles.breakdown}>
99+
<h4>Payment Breakdown:</h4>
100+
<ul className={`${styles.breakdownList} body-main`}>
101+
<li>
102+
<span>Base amount:</span>
103+
<span>
104+
$
105+
{totalAmount.toFixed(2)}
106+
</span>
107+
</li>
108+
<li>
109+
<span>
110+
Tax Witholding (
111+
{nullToZero(props.walletDetails.taxWithholdingPercentage)}
112+
%):
113+
</span>
114+
<span>
115+
$
116+
{taxWithholdAmount.toFixed(2)}
117+
</span>
118+
</li>
119+
<li>
120+
<span>Processing fee:</span>
121+
<span>
122+
$
123+
{feesAmount.toFixed(2)}
124+
</span>
125+
</li>
126+
</ul>
127+
<hr />
128+
<div className={`${styles.summary} body-main-bold`}>
129+
<span>Net amount after fees:</span>
130+
<span>
131+
$
132+
{netAmount.toFixed(2)}
133+
</span>
134+
</div>
135+
{props.walletDetails?.primaryCurrency && props.walletDetails.primaryCurrency !== 'USD' && (
136+
<div className={`${styles.alert} body-main-medium`}>
137+
Net amount will be converted to
138+
{' '}
139+
{props.walletDetails.primaryCurrency}
140+
{' '}
141+
with a 2% conversion fee applied.
142+
</div>
143+
)}
144+
</div>
145+
<div className={`${styles.taxesFooterRow} body-main`}>
146+
You can adjust your payout settings to customize your estimated payment fee
147+
and tax withholding percentage in the
148+
{' '}
149+
<Link to='#payout'>Payout</Link>
150+
{' '}
151+
section.
152+
</div>
153+
</>
154+
)}
155+
</ConfirmModal>
156+
{otpModal}
157+
</>
158+
)
159+
}
160+
161+
export default ConfirmPaymentModal

src/apps/wallet/src/home/tabs/winnings/WinningsTab.tsx

Lines changed: 21 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
/* eslint-disable max-len */
22
/* eslint-disable react/jsx-no-bind */
3-
import { toast } from 'react-toastify'
4-
import { AxiosError } from 'axios'
5-
import { Link } from 'react-router-dom'
63
import React, { FC, useCallback, useEffect } from 'react'
74

8-
import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui'
5+
import { Collapsible, LoadingCircles } from '~/libs/ui'
96
import { UserProfile } from '~/libs/core'
107

11-
import { getPayments, processWinningsPayments } from '../../../lib/services/wallet'
8+
import { getPayments } from '../../../lib/services/wallet'
129
import { Winning, WinningDetail } from '../../../lib/models/WinningDetail'
1310
import { FilterBar } from '../../../lib'
14-
import { ConfirmFlowData } from '../../../lib/models/ConfirmFlowData'
1511
import { PaginationInfo } from '../../../lib/models/PaginationInfo'
1612
import { useWalletDetails, WalletDetailsResponse } from '../../../lib/hooks/use-wallet-details'
17-
import { nullToZero } from '../../../lib/util'
13+
import { WalletDetails } from '../../../lib/models/WalletDetails'
1814
import PaymentsTable from '../../../lib/components/payments-table/PaymentTable'
1915

16+
import ConfirmPaymentModal from './ConfirmPayment.modal'
2017
import styles from './Winnings.module.scss'
2118

2219
interface ListViewProps {
@@ -78,7 +75,7 @@ const formatCurrency = (amountStr: string, currency: string): string => {
7875
}
7976

8077
const ListView: FC<ListViewProps> = (props: ListViewProps) => {
81-
const [confirmFlow, setConfirmFlow] = React.useState<ConfirmFlowData | undefined>(undefined)
78+
const [confirmPayments, setConfirmPayments] = React.useState<Winning[]>()
8279
const [winnings, setWinnings] = React.useState<ReadonlyArray<Winning>>([])
8380
const [selectedPayments, setSelectedPayments] = React.useState<{ [paymentId: string]: Winning }>({})
8481
const [isLoading, setIsLoading] = React.useState<boolean>(false)
@@ -146,133 +143,22 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
146143
}
147144
}, [props.profile.userId, convertToWinnings, filters, pagination.currentPage, pagination.pageSize])
148145

149-
const renderConfirmModalContent = React.useMemo(() => {
150-
if (confirmFlow?.content === undefined) {
151-
return undefined
152-
}
153-
154-
if (typeof confirmFlow?.content === 'function') {
155-
return confirmFlow?.content()
156-
}
157-
158-
return confirmFlow?.content
159-
}, [confirmFlow])
160-
161146
useEffect(() => {
162147
fetchWinnings()
163148
}, [fetchWinnings])
164149

165-
const processPayouts = async (winningIds: string[]): Promise<void> => {
166-
setSelectedPayments({})
167-
168-
toast.info('Processing payments...', {
169-
position: toast.POSITION.BOTTOM_RIGHT,
170-
})
171-
try {
172-
await processWinningsPayments(winningIds)
173-
toast.success('Payments processed successfully!', {
174-
position: toast.POSITION.BOTTOM_RIGHT,
175-
})
176-
} catch (error) {
177-
let message = 'Failed to process payments. Please try again later.'
178-
179-
if (error instanceof AxiosError) {
180-
message = error.response?.data?.error?.message ?? error.response?.data?.message ?? error.message ?? ''
181-
182-
message = message.charAt(0)
183-
.toUpperCase() + message.slice(1)
184-
}
185-
186-
toast.error(message, {
187-
position: toast.POSITION.BOTTOM_RIGHT,
188-
})
189-
}
190-
191-
fetchWinnings()
192-
}
193-
194150
function handlePayMeClick(
195-
paymentIds: { [paymentId: string]: Winning },
196-
totalAmountStr: string,
151+
payments: { [paymentId: string]: Winning },
197152
): void {
198-
const totalAmount = parseFloat(totalAmountStr)
199-
const taxWithholdAmount = (parseFloat(nullToZero(walletDetails?.taxWithholdingPercentage ?? '0')) * totalAmount) / 100
200-
const feesAmount = parseFloat(nullToZero(walletDetails?.estimatedFees ?? '0'))
201-
const netAmount = totalAmount - taxWithholdAmount - feesAmount
153+
setConfirmPayments(Object.values(payments))
154+
}
202155

203-
setConfirmFlow({
204-
action: 'Confirm Payment',
205-
callback: () => processPayouts(Object.keys(paymentIds)),
206-
content: (
207-
<>
208-
<div className={`${styles.processing} body-medium-normal`}>
209-
Processing Payment: $
210-
{totalAmountStr}
211-
{' '}
212-
</div>
213-
{walletDetails && (
214-
<>
215-
<div className={styles.breakdown}>
216-
<h4>Payment Breakdown:</h4>
217-
<ul className={`${styles.breakdownList} body-main`}>
218-
<li>
219-
<span>Base amount:</span>
220-
<span>
221-
$
222-
{totalAmountStr}
223-
</span>
224-
</li>
225-
<li>
226-
<span>
227-
Tax Witholding (
228-
{nullToZero(walletDetails.taxWithholdingPercentage)}
229-
%):
230-
</span>
231-
<span>
232-
$
233-
{taxWithholdAmount.toFixed(2)}
234-
</span>
235-
</li>
236-
<li>
237-
<span>Processing fee:</span>
238-
<span>
239-
$
240-
{feesAmount.toFixed(2)}
241-
</span>
242-
</li>
243-
</ul>
244-
<hr />
245-
<div className={`${styles.summary} body-main-bold`}>
246-
<span>Net amount after fees:</span>
247-
<span>
248-
$
249-
{netAmount.toFixed(2)}
250-
</span>
251-
</div>
252-
{walletDetails?.primaryCurrency && walletDetails.primaryCurrency !== 'USD' && (
253-
<div className={`${styles.alert} body-main-medium`}>
254-
Net amount will be converted to
255-
{' '}
256-
{walletDetails.primaryCurrency}
257-
{' '}
258-
with a 2% conversion fee applied.
259-
</div>
260-
)}
261-
</div>
262-
<div className={`${styles.taxesFooterRow} body-main`}>
263-
You can adjust your payout settings to customize your estimated payment fee
264-
and tax withholding percentage in the
265-
{' '}
266-
<Link to='#payout'>Payout</Link>
267-
{' '}
268-
section.
269-
</div>
270-
</>
271-
)}
272-
</>
273-
),
274-
title: 'Payment Confirmation',
275-
})
156+
function handleCloseConfirmModal(isDone?: boolean): void {
157+
setConfirmPayments(undefined)
158+
setSelectedPayments({})
159+
if (isDone) {
160+
fetchWinnings()
161+
}
276162
}
277163

278164
return (
@@ -461,23 +347,13 @@ const ListView: FC<ListViewProps> = (props: ListViewProps) => {
461347
</Collapsible>
462348
</div>
463349
</div>
464-
{confirmFlow && (
465-
<ConfirmModal
466-
size='lg'
467-
maxWidth='610px'
468-
title={confirmFlow.title}
469-
action={confirmFlow.action}
470-
onClose={function onClose() {
471-
setConfirmFlow(undefined)
472-
}}
473-
onConfirm={function onConfirm() {
474-
confirmFlow.callback?.()
475-
setConfirmFlow(undefined)
476-
}}
477-
open={confirmFlow !== undefined}
478-
>
479-
<div>{renderConfirmModalContent}</div>
480-
</ConfirmModal>
350+
{confirmPayments && (
351+
<ConfirmPaymentModal
352+
userEmail={props.profile.email}
353+
payments={confirmPayments}
354+
walletDetails={walletDetails as WalletDetails}
355+
onClose={handleCloseConfirmModal}
356+
/>
481357
)}
482358
</>
483359
)

0 commit comments

Comments
 (0)