@huma-finance/widgets 0.0.59-beta.485 → 0.0.59-beta.486

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ import {
2
+ CampaignService,
3
+ checkIsDev,
4
+ formatNumber,
5
+ useAuthErrorHandling,
6
+ } from '@huma-finance/shared'
7
+ import { Box, css, useTheme } from '@mui/material'
8
+ import { useWeb3React } from '@web3-react/core'
9
+ import React, { useEffect, useState } from 'react'
10
+ import { useDispatch } from 'react-redux'
11
+
12
+ import { setError } from '../../../store/widgets.reducers'
13
+ import { BottomButton } from '../../BottomButton'
14
+ import { CongratulationsIcon, HumaPointsIcon, RibbonIcon } from '../../icons'
15
+ import { LoadingModal } from '../../LoadingModal'
16
+ import { SignIn } from '../../SignIn'
17
+
18
+ enum STATE {
19
+ Loading = 'Loading',
20
+ SignIn = 'SignIn',
21
+ Congrats = 'Congrats',
22
+ }
23
+
24
+ const ERROR_MESSAGE =
25
+ 'Failed to update wallet points. Be assured that your points will be added later.'
26
+
27
+ type Props = {
28
+ transactionHash: string
29
+ lpConfig: { withdrawalLockoutPeriodInDays: number }
30
+ handleAction: () => void
31
+ }
32
+
33
+ export function PointsEarned({
34
+ transactionHash,
35
+ lpConfig,
36
+ handleAction,
37
+ }: Props): React.ReactElement {
38
+ const theme = useTheme()
39
+ const dispatch = useDispatch()
40
+ const { account, chainId } = useWeb3React()
41
+ const [pointsAccumulated, setPointsAccumulated] = useState<
42
+ number | undefined
43
+ >()
44
+ const lockupMonths = Math.round(lpConfig.withdrawalLockoutPeriodInDays / 30)
45
+ const monthText =
46
+ lockupMonths > 1 ? `${lockupMonths} months` : `${lockupMonths} month`
47
+
48
+ const {
49
+ errorType,
50
+ setError: setAuthError,
51
+ isWalletOwnershipVerified,
52
+ isWalletOwnershipVerificationRequired,
53
+ } = useAuthErrorHandling(checkIsDev())
54
+ const [walletOwnership, setWalletOwnership] = useState<boolean | undefined>()
55
+ const [state, setState] = useState<STATE>(STATE.Loading)
56
+
57
+ useEffect(() => {
58
+ if (isWalletOwnershipVerificationRequired) {
59
+ setState(STATE.Loading)
60
+ }
61
+ }, [isWalletOwnershipVerificationRequired])
62
+
63
+ useEffect(() => {
64
+ if (isWalletOwnershipVerified) {
65
+ setWalletOwnership(true)
66
+ }
67
+ }, [isWalletOwnershipVerified])
68
+
69
+ useEffect(() => {
70
+ const checkWalletOwnership = async () => {
71
+ const ownership = await CampaignService.checkWalletOwnership(account!)
72
+ setWalletOwnership(ownership)
73
+ if (!ownership) {
74
+ setAuthError('WalletNotSignInException')
75
+ }
76
+ }
77
+ checkWalletOwnership()
78
+ }, [account, setAuthError])
79
+
80
+ useEffect(() => {
81
+ if (errorType === 'NotSignedIn') {
82
+ setState(STATE.SignIn)
83
+ } else if (errorType === 'UserRejected') {
84
+ dispatch(
85
+ setError({
86
+ errorMessage: 'User has rejected the transaction.',
87
+ }),
88
+ )
89
+ } else if (errorType === 'Other') {
90
+ dispatch(
91
+ setError({
92
+ errorMessage: ERROR_MESSAGE,
93
+ }),
94
+ )
95
+ }
96
+ }, [dispatch, errorType])
97
+
98
+ useEffect(() => {
99
+ const updateWalletPoints = async () => {
100
+ if (walletOwnership) {
101
+ try {
102
+ const result = await CampaignService.updateWalletPoints(
103
+ chainId!,
104
+ account!,
105
+ transactionHash,
106
+ )
107
+ if (!result.pointsAccumulated) {
108
+ dispatch(
109
+ setError({
110
+ errorMessage: ERROR_MESSAGE,
111
+ }),
112
+ )
113
+ }
114
+ setPointsAccumulated(result.pointsAccumulated)
115
+ setState(STATE.Congrats)
116
+ } catch (error) {
117
+ dispatch(
118
+ setError({
119
+ errorMessage: ERROR_MESSAGE,
120
+ }),
121
+ )
122
+ }
123
+ }
124
+ }
125
+ updateWalletPoints()
126
+ }, [account, chainId, dispatch, transactionHash, walletOwnership])
127
+
128
+ const styles = {
129
+ wrapper: css`
130
+ height: 518px;
131
+ position: relative;
132
+ `,
133
+ congrats: css`
134
+ margin: ${theme.spacing(-4, -4.5, 0, -4.5)};
135
+ `,
136
+ ribbon: css`
137
+ position: relative;
138
+ ${theme.cssMixins.rowHCentered};
139
+ font-family: 'Uni-Neue-Bold';
140
+ margin-top: ${theme.spacing(-8)};
141
+ color: #fbfbfc;
142
+ font-size: 20px;
143
+ line-height: 160%;
144
+ letter-spacing: 0.15px;
145
+ `,
146
+ ribbonContent: css`
147
+ ${theme.cssMixins.rowVCentered};
148
+ position: absolute;
149
+ top: ${theme.spacing(1)};
150
+
151
+ svg {
152
+ margin-right: ${theme.spacing(1)};
153
+ }
154
+ `,
155
+ entirePoints: css`
156
+ color: ${theme.palette.text.tertiary};
157
+ text-align: center;
158
+ font-family: 'Uni-Neue-Bold';
159
+ font-size: 20px;
160
+ line-height: 160%;
161
+ letter-spacing: 0.15px;
162
+ margin-top: ${theme.spacing(7)};
163
+ `,
164
+ entirePointsDetails: css`
165
+ color: ${theme.palette.text.tertiary};
166
+ text-align: center;
167
+ font-family: 'Uni-Neue-Regular';
168
+ font-size: 16px;
169
+ line-height: 150%;
170
+ letter-spacing: 0.15px;
171
+ margin-top: ${theme.spacing(4)};
172
+ `,
173
+ everyday: css`
174
+ font-family: 'Uni-Neue-Bold';
175
+ `,
176
+ }
177
+
178
+ if (state === STATE.SignIn) {
179
+ return (
180
+ <SignIn description='Please sign in to check the points that you earned.' />
181
+ )
182
+ }
183
+
184
+ if (state === STATE.Congrats) {
185
+ return (
186
+ <Box css={styles.wrapper}>
187
+ <Box css={styles.congrats}>
188
+ <CongratulationsIcon />
189
+ </Box>
190
+ <Box css={styles.ribbon}>
191
+ <RibbonIcon />
192
+ <Box css={styles.ribbonContent}>
193
+ <HumaPointsIcon />
194
+ <Box>{formatNumber(pointsAccumulated)} Points</Box>
195
+ </Box>
196
+ </Box>
197
+ <Box css={styles.entirePoints}>
198
+ <Box>Congratulations,</Box>
199
+ <Box>you've earned {pointsAccumulated} points</Box>
200
+ </Box>
201
+ <Box css={styles.entirePointsDetails}>
202
+ You'll earn points <span css={styles.everyday}>everyday</span> for{' '}
203
+ {monthText} straight. Plus, If you keep your investment till after{' '}
204
+ {monthText}, you’ll gain extra points daily.
205
+ </Box>
206
+ <BottomButton variant='contained' onClick={handleAction}>
207
+ GREAT
208
+ </BottomButton>
209
+ </Box>
210
+ )
211
+ }
212
+
213
+ return <LoadingModal title='SUPPLY' />
214
+ }
@@ -0,0 +1,425 @@
1
+ import {
2
+ CAMPAIGN_REFERENCE_CODE,
3
+ CampaignService,
4
+ checkIsDev,
5
+ formatNumber,
6
+ IdentityServiceV2,
7
+ IdentityVerificationStatusV2,
8
+ KYCCopy,
9
+ PoolInfoV2,
10
+ TrancheType,
11
+ useAuthErrorHandling,
12
+ VerificationStatusResultV2,
13
+ } from '@huma-finance/shared'
14
+ import { Box, css, useTheme } from '@mui/material'
15
+ import { useWeb3React } from '@web3-react/core'
16
+ import { BigNumber } from 'ethers'
17
+ import Persona, { Client } from 'persona'
18
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
19
+
20
+ import { Campaign } from '..'
21
+ import { useAppDispatch } from '../../../../hooks/useRedux'
22
+ import { setError, setStep } from '../../../../store/widgets.reducers'
23
+ import { WIDGET_STEP } from '../../../../store/widgets.store'
24
+ import { BottomButton } from '../../../BottomButton'
25
+ import { ApproveLenderImg } from '../../../images'
26
+ import { LoadingModal } from '../../../LoadingModal'
27
+ import { WrapperModal } from '../../../WrapperModal'
28
+
29
+ type LoadingType = 'verificationStatus' | 'startKYC' | 'approveLender'
30
+
31
+ const LoadingCopiesByType: {
32
+ [key: string]: {
33
+ description: string
34
+ }
35
+ } = {
36
+ verificationStatus: {
37
+ description: `Checking your verification status...`,
38
+ },
39
+ startKYC: {
40
+ description: `Starting KYC...`,
41
+ },
42
+ approveLender: {
43
+ description: `Approving as lender...`,
44
+ },
45
+ }
46
+
47
+ type Props = {
48
+ poolInfo: PoolInfoV2
49
+ isUniTranche: boolean
50
+ minDepositAmount: BigNumber
51
+ campaign?: Campaign
52
+ changeTranche: (tranche: TrancheType) => void
53
+ handleClose: () => void
54
+ }
55
+
56
+ export function PersonaEvaluation({
57
+ poolInfo,
58
+ isUniTranche,
59
+ minDepositAmount: minDepositAmountBN,
60
+ campaign,
61
+ changeTranche,
62
+ handleClose,
63
+ }: Props): React.ReactElement | null {
64
+ const theme = useTheme()
65
+ const isDev = checkIsDev()
66
+ const dispatch = useAppDispatch()
67
+ const { account, chainId } = useWeb3React()
68
+ const {
69
+ errorType,
70
+ setError: setAuthError,
71
+ isWalletOwnershipVerified,
72
+ isWalletOwnershipVerificationRequired,
73
+ } = useAuthErrorHandling(isDev)
74
+ const KYCCopies = poolInfo.KYC!.Persona!
75
+ const [loadingType, setLoadingType] = useState<LoadingType>()
76
+ const [kycCopy, setKYCCopy] = useState<KYCCopy>(KYCCopies.verifyIdentity)
77
+ const [inquiryId, setInquiryId] = useState<string>()
78
+ const [sessionToken, setSessionToken] = useState<string>()
79
+ const [verificationStatus, setVerificationStatus] =
80
+ useState<VerificationStatusResultV2>()
81
+ const [showPersonaClient, setShowPersonaClient] = useState<boolean>(false)
82
+ const [campaignBasePoints, setCampaignBasePoints] = useState<number>(0)
83
+ const isKYCCompletedRef = useRef<boolean>(false)
84
+ const isActionOngoingRef = useRef<boolean>(false)
85
+ const isKYCResumedRef = useRef<boolean>(false)
86
+
87
+ useEffect(() => {
88
+ const createNewWallet = async () => {
89
+ if (isWalletOwnershipVerified && campaign && account) {
90
+ await CampaignService.createNewWallet(
91
+ account,
92
+ localStorage.getItem(CAMPAIGN_REFERENCE_CODE) ?? undefined,
93
+ )
94
+ }
95
+ }
96
+ createNewWallet()
97
+ }, [account, campaign, isWalletOwnershipVerified])
98
+
99
+ useEffect(() => {
100
+ const getBasePoints = async () => {
101
+ if (campaign) {
102
+ const estimatedPoints = await CampaignService.getEstimatedPoints(
103
+ campaign.campaignGroupId,
104
+ minDepositAmountBN.toString(),
105
+ )
106
+ const campaignPoints = estimatedPoints.find(
107
+ (item) => item.campaignId === campaign.id,
108
+ )
109
+ if (campaignPoints) {
110
+ const { juniorTranchePoints, seniorTranchePoints } = campaignPoints
111
+ if (isUniTranche) {
112
+ setCampaignBasePoints(juniorTranchePoints ?? 0)
113
+ } else {
114
+ setCampaignBasePoints(
115
+ juniorTranchePoints < seniorTranchePoints
116
+ ? juniorTranchePoints
117
+ : seniorTranchePoints,
118
+ )
119
+ }
120
+ }
121
+ }
122
+ }
123
+ getBasePoints()
124
+ }, [campaign, isUniTranche, minDepositAmountBN])
125
+
126
+ const approveLender = useCallback(async () => {
127
+ try {
128
+ isActionOngoingRef.current = true
129
+ setLoadingType('approveLender')
130
+ await IdentityServiceV2.approveLender(
131
+ account!,
132
+ chainId!,
133
+ poolInfo.juniorTrancheVault,
134
+ isDev,
135
+ )
136
+ if (!isUniTranche) {
137
+ await IdentityServiceV2.approveLender(
138
+ account!,
139
+ chainId!,
140
+ poolInfo.seniorTrancheVault,
141
+ isDev,
142
+ )
143
+ }
144
+ if (isUniTranche) {
145
+ changeTranche('junior')
146
+ dispatch(setStep(WIDGET_STEP.ChooseAmount))
147
+ } else {
148
+ dispatch(setStep(WIDGET_STEP.ChooseTranche))
149
+ }
150
+ } finally {
151
+ isActionOngoingRef.current = false
152
+ setLoadingType(undefined)
153
+ }
154
+ }, [
155
+ account,
156
+ chainId,
157
+ changeTranche,
158
+ dispatch,
159
+ isDev,
160
+ isUniTranche,
161
+ poolInfo.juniorTrancheVault,
162
+ poolInfo.seniorTrancheVault,
163
+ ])
164
+
165
+ const checkVerificationStatus = useCallback(async () => {
166
+ if (isActionOngoingRef.current || isKYCResumedRef.current) {
167
+ return
168
+ }
169
+ try {
170
+ if (account && chainId) {
171
+ isActionOngoingRef.current = true
172
+ setLoadingType('verificationStatus')
173
+ const verificationStatus =
174
+ await IdentityServiceV2.getVerificationStatusV2(
175
+ account,
176
+ chainId,
177
+ isDev,
178
+ )
179
+ setVerificationStatus(verificationStatus)
180
+ setInquiryId(verificationStatus.personaInquiryId)
181
+
182
+ switch (verificationStatus.status) {
183
+ case IdentityVerificationStatusV2.NOT_STARTED: {
184
+ const startVerificationResult =
185
+ await IdentityServiceV2.startVerification(account, chainId, isDev)
186
+ setInquiryId(startVerificationResult.personaInquiryId)
187
+ setKYCCopy(KYCCopies.verifyIdentity)
188
+ setLoadingType(undefined)
189
+ break
190
+ }
191
+
192
+ case IdentityVerificationStatusV2.CREATED: {
193
+ setKYCCopy(KYCCopies.verifyIdentity)
194
+ setLoadingType(undefined)
195
+ break
196
+ }
197
+
198
+ case IdentityVerificationStatusV2.PENDING: {
199
+ if (!isKYCCompletedRef.current) {
200
+ const resumeVerificationResult =
201
+ await IdentityServiceV2.resumeVerification(
202
+ account,
203
+ chainId,
204
+ isDev,
205
+ )
206
+ verificationStatus.status = resumeVerificationResult.status
207
+ setVerificationStatus(verificationStatus)
208
+ setSessionToken(resumeVerificationResult.sessionToken)
209
+ setLoadingType(undefined)
210
+ isKYCResumedRef.current = true
211
+ } else {
212
+ setLoadingType('verificationStatus')
213
+ }
214
+ setKYCCopy(KYCCopies.verifyIdentity)
215
+ break
216
+ }
217
+
218
+ case IdentityVerificationStatusV2.EXPIRED: {
219
+ const resumeVerificationResult =
220
+ await IdentityServiceV2.resumeVerification(
221
+ account,
222
+ chainId,
223
+ isDev,
224
+ )
225
+ verificationStatus.status = resumeVerificationResult.status
226
+ setVerificationStatus(verificationStatus)
227
+ setSessionToken(resumeVerificationResult.sessionToken)
228
+ setKYCCopy(KYCCopies.verifyIdentity)
229
+ setLoadingType(undefined)
230
+ isKYCResumedRef.current = true
231
+ break
232
+ }
233
+
234
+ case IdentityVerificationStatusV2.APPROVED: {
235
+ await approveLender()
236
+ break
237
+ }
238
+
239
+ case IdentityVerificationStatusV2.DECLINED: {
240
+ setKYCCopy(KYCCopies.verificationDeclined)
241
+ setLoadingType(undefined)
242
+ break
243
+ }
244
+
245
+ case IdentityVerificationStatusV2.NEEDS_REVIEW: {
246
+ setKYCCopy(KYCCopies.verificationNeedsReview)
247
+ setLoadingType(undefined)
248
+ break
249
+ }
250
+
251
+ default:
252
+ break
253
+ }
254
+ }
255
+ } catch (e: unknown) {
256
+ setAuthError(e)
257
+ } finally {
258
+ isActionOngoingRef.current = false
259
+ }
260
+ }, [KYCCopies, account, approveLender, chainId, isDev, setAuthError])
261
+
262
+ useEffect(() => {
263
+ checkVerificationStatus()
264
+ }, [checkVerificationStatus])
265
+
266
+ useEffect(() => {
267
+ if (isWalletOwnershipVerificationRequired && isWalletOwnershipVerified) {
268
+ checkVerificationStatus()
269
+ }
270
+ }, [
271
+ checkVerificationStatus,
272
+ isWalletOwnershipVerificationRequired,
273
+ isWalletOwnershipVerified,
274
+ ])
275
+
276
+ useEffect(() => {
277
+ const interval = setInterval(() => {
278
+ if (
279
+ verificationStatus?.status &&
280
+ [
281
+ IdentityVerificationStatusV2.CREATED,
282
+ IdentityVerificationStatusV2.PENDING,
283
+ ].includes(verificationStatus.status)
284
+ ) {
285
+ checkVerificationStatus()
286
+ }
287
+ }, 10 * 1000)
288
+ // eslint-disable-next-line consistent-return
289
+ return () => clearInterval(interval)
290
+ }, [checkVerificationStatus, verificationStatus?.status])
291
+
292
+ useEffect(() => {
293
+ if (errorType === 'NotSignedIn') {
294
+ setLoadingType(undefined)
295
+ setKYCCopy(KYCCopies.signInRequired)
296
+ } else if (errorType === 'UserRejected') {
297
+ dispatch(
298
+ setError({
299
+ errorMessage: 'User has rejected the transaction.',
300
+ }),
301
+ )
302
+ } else if (errorType === 'Other') {
303
+ dispatch(
304
+ setError({
305
+ errorMessage: 'Something went wrong. Please try again later.',
306
+ }),
307
+ )
308
+ }
309
+ }, [KYCCopies.signInRequired, dispatch, errorType])
310
+
311
+ useEffect(() => {
312
+ const modalElement = document.getElementById('huma-modal')
313
+ if (modalElement) {
314
+ modalElement.style.visibility = showPersonaClient ? 'hidden' : 'visible'
315
+ }
316
+ }, [showPersonaClient])
317
+
318
+ const startKYC = async () => {
319
+ isActionOngoingRef.current = true
320
+ isKYCResumedRef.current = false
321
+ setLoadingType('startKYC')
322
+ const client: Client = new Persona.Client({
323
+ inquiryId,
324
+ sessionToken,
325
+ onReady: () => {
326
+ client.open()
327
+ setShowPersonaClient(true)
328
+ },
329
+ onComplete: () => {
330
+ isKYCCompletedRef.current = true
331
+ isActionOngoingRef.current = false
332
+ setShowPersonaClient(false)
333
+ checkVerificationStatus()
334
+ },
335
+ onCancel: () => {
336
+ isActionOngoingRef.current = false
337
+ setShowPersonaClient(false)
338
+ setLoadingType(undefined)
339
+ },
340
+ onError: () => {
341
+ isActionOngoingRef.current = false
342
+ setShowPersonaClient(false)
343
+ setLoadingType(undefined)
344
+ },
345
+ })
346
+ }
347
+
348
+ const handleAction = () => {
349
+ if (verificationStatus) {
350
+ switch (verificationStatus.status) {
351
+ case IdentityVerificationStatusV2.NOT_STARTED:
352
+ case IdentityVerificationStatusV2.CREATED:
353
+ case IdentityVerificationStatusV2.PENDING:
354
+ case IdentityVerificationStatusV2.EXPIRED:
355
+ startKYC()
356
+ break
357
+
358
+ case IdentityVerificationStatusV2.DECLINED:
359
+ case IdentityVerificationStatusV2.NEEDS_REVIEW:
360
+ handleClose()
361
+ break
362
+
363
+ default:
364
+ break
365
+ }
366
+ }
367
+ }
368
+
369
+ const styles = {
370
+ iconWrapper: css`
371
+ ${theme.cssMixins.rowCentered};
372
+ margin-top: ${theme.spacing(8)};
373
+ & > img {
374
+ width: 144px;
375
+ }
376
+ `,
377
+ description: css`
378
+ text-align: justify;
379
+ margin-top: ${theme.spacing(10)};
380
+ padding: ${theme.spacing(0, 2)};
381
+ font-family: 'Uni-Neue-Regular';
382
+ font-size: 16px;
383
+ color: #a8a1b2;
384
+ `,
385
+ points: css`
386
+ color: #b246ff;
387
+ font-family: 'Uni-Neue-Bold';
388
+ `,
389
+ }
390
+
391
+ if (!loadingType) {
392
+ return (
393
+ <WrapperModal title={kycCopy.title}>
394
+ <Box css={styles.iconWrapper}>
395
+ <img src={ApproveLenderImg} alt='approve-lender' />
396
+ </Box>
397
+ {campaign && kycCopy === KYCCopies.verifyIdentity ? (
398
+ <Box css={styles.description}>
399
+ <span>You’ll be rewarded with a minimum of</span>
400
+ <span css={styles.points}>
401
+ {' '}
402
+ {formatNumber(campaignBasePoints)} Huma points{' '}
403
+ </span>
404
+ <span>after completing KYC and your first investment.</span>
405
+ </Box>
406
+ ) : (
407
+ <Box css={styles.description}>{kycCopy.description}</Box>
408
+ )}
409
+
410
+ {Boolean(kycCopy.buttonText) && (
411
+ <BottomButton variant='contained' onClick={handleAction}>
412
+ {kycCopy.buttonText}
413
+ </BottomButton>
414
+ )}
415
+ </WrapperModal>
416
+ )
417
+ }
418
+
419
+ return (
420
+ <LoadingModal
421
+ title='Lender Approval'
422
+ description={LoadingCopiesByType[loadingType].description}
423
+ />
424
+ )
425
+ }