Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
frontend
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
vicotor
frontend
Commits
7b45a38a
Unverified
Commit
7b45a38a
authored
Mar 25, 2025
by
Max Alekseenko
Committed by
GitHub
Mar 25, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support custom ref codes (Merits) (#2631)
support custom ref codes
parent
35135fff
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
56 additions
and
43 deletions
+56
-43
.env.eth_sepolia
configs/envs/.env.eth_sepolia
+1
-1
rewards.tsx
lib/contexts/rewards.tsx
+12
-5
rewards.ts
types/api/rewards.ts
+2
-0
RewardsLoginModal.tsx
ui/rewards/login/RewardsLoginModal.tsx
+16
-15
CongratsStepContent.tsx
ui/rewards/login/steps/CongratsStepContent.tsx
+8
-5
LoginStepContent.tsx
ui/rewards/login/steps/LoginStepContent.tsx
+17
-17
No files found.
configs/envs/.env.eth_sepolia
View file @
7b45a38a
...
...
@@ -68,4 +68,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
\ No newline at end of file
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
lib/contexts/rewards.tsx
View file @
7b45a38a
...
...
@@ -45,7 +45,7 @@ type TRewardsContext = {
openLoginModal
:
()
=>
void
;
closeLoginModal
:
()
=>
void
;
saveApiToken
:
(
token
:
string
|
undefined
)
=>
void
;
login
:
(
refCode
:
string
)
=>
Promise
<
{
isNewUser
?:
boolean
;
invalidRefCodeError
?:
boolean
}
>
;
login
:
(
refCode
:
string
)
=>
Promise
<
{
isNewUser
:
boolean
;
reward
:
string
|
null
;
invalidRefCodeError
?:
boolean
}
>
;
claim
:
()
=>
Promise
<
void
>
;
};
...
...
@@ -70,7 +70,7 @@ const initialState = {
openLoginModal
:
()
=>
{},
closeLoginModal
:
()
=>
{},
saveApiToken
:
()
=>
{},
login
:
async
()
=>
({}),
login
:
async
()
=>
({
isNewUser
:
false
,
reward
:
null
}),
claim
:
async
()
=>
{},
};
...
...
@@ -216,10 +216,14 @@ export function RewardsContextProvider({ children }: Props) {
apiFetch
(
'
rewards_nonce
'
)
as
Promise
<
RewardsNonceResponse
>
,
refCode
?
apiFetch
(
'
rewards_check_ref_code
'
,
{
pathParams
:
{
code
:
refCode
}
})
as
Promise
<
RewardsCheckRefCodeResponse
>
:
Promise
.
resolve
({
valid
:
true
}),
Promise
.
resolve
({
valid
:
true
,
reward
:
null
}),
]);
if
(
!
checkCodeResponse
.
valid
)
{
return
{
invalidRefCodeError
:
true
};
return
{
invalidRefCodeError
:
true
,
isNewUser
:
false
,
reward
:
null
,
};
}
const
message
=
getMessageToSign
(
address
,
nonceResponse
.
nonce
,
checkUserQuery
.
data
?.
exists
,
refCode
);
const
signature
=
await
signMessageAsync
({
message
});
...
...
@@ -234,7 +238,10 @@ export function RewardsContextProvider({ children }: Props) {
},
})
as
RewardsLoginResponse
;
saveApiToken
(
loginResponse
.
token
);
return
{
isNewUser
:
loginResponse
.
created
};
return
{
isNewUser
:
loginResponse
.
created
,
reward
:
checkCodeResponse
.
reward
,
};
}
catch
(
_error
)
{
errorToast
(
_error
);
throw
_error
;
...
...
types/api/rewards.ts
View file @
7b45a38a
...
...
@@ -12,6 +12,8 @@ export type RewardsConfigResponse = {
export
type
RewardsCheckRefCodeResponse
=
{
valid
:
boolean
;
is_custom
:
boolean
;
reward
:
string
|
null
;
};
export
type
RewardsNonceResponse
=
{
...
...
ui/rewards/login/RewardsLoginModal.tsx
View file @
7b45a38a
import
{
Modal
,
ModalOverlay
,
ModalContent
,
ModalHeader
,
ModalCloseButton
,
ModalBody
,
use
Boolean
,
use
Disclosure
}
from
'
@chakra-ui/react
'
;
import
React
,
{
useCallback
,
useEffect
}
from
'
react
'
;
import
{
Modal
,
ModalOverlay
,
ModalContent
,
ModalHeader
,
ModalCloseButton
,
ModalBody
,
useDisclosure
}
from
'
@chakra-ui/react
'
;
import
React
,
{
useCallback
,
useEffect
,
useState
}
from
'
react
'
;
import
type
{
Screen
}
from
'
ui/snippets/auth/types
'
;
...
...
@@ -25,24 +25,25 @@ const RewardsLoginModal = () => {
const
isMobile
=
useIsMobile
();
const
{
isLoginModalOpen
,
closeLoginModal
}
=
useRewardsContext
();
const
[
isLoginStep
,
setIsLoginStep
]
=
useBoolean
(
true
);
const
[
isReferral
,
setIsReferral
]
=
useBoolean
(
false
);
const
[
authModalInitialScreen
,
setAuthModalInitialScreen
]
=
React
.
useState
<
Screen
>
();
const
[
isLoginStep
,
setIsLoginStep
]
=
useState
(
true
);
const
[
isReferral
,
setIsReferral
]
=
useState
(
false
);
const
[
customReferralReward
,
setCustomReferralReward
]
=
useState
<
string
|
null
>
(
null
);
const
[
authModalInitialScreen
,
setAuthModalInitialScreen
]
=
useState
<
Screen
>
();
const
authModal
=
useDisclosure
();
useEffect
(()
=>
{
if
(
!
isLoginModalOpen
)
{
setIsLoginStep
.
on
();
setIsReferral
.
off
();
setIsLoginStep
(
true
);
setIsReferral
(
false
);
setCustomReferralReward
(
null
);
}
},
[
isLoginModalOpen
,
setIsLoginStep
,
setIsReferral
]);
},
[
isLoginModalOpen
]);
const
goNext
=
useCallback
((
isReferral
:
boolean
)
=>
{
if
(
isReferral
)
{
setIsReferral
.
on
();
}
setIsLoginStep
.
off
();
},
[
setIsLoginStep
,
setIsReferral
]);
const
goNext
=
useCallback
((
isReferral
:
boolean
,
reward
:
string
|
null
)
=>
{
setIsReferral
(
isReferral
);
setCustomReferralReward
(
reward
);
setIsLoginStep
(
false
);
},
[]);
const
handleAuthModalOpen
=
useCallback
((
isAuth
:
boolean
,
trySharedLogin
?:
boolean
)
=>
{
setAuthModalInitialScreen
({
type
:
'
connect_wallet
'
,
isAuth
,
loginToRewards
:
trySharedLogin
});
...
...
@@ -74,7 +75,7 @@ const RewardsLoginModal = () => {
<
ModalBody
mb=
{
0
}
>
{
isLoginStep
?
<
LoginStepContent
goNext=
{
goNext
}
openAuthModal=
{
handleAuthModalOpen
}
closeModal=
{
closeLoginModal
}
/>
:
<
CongratsStepContent
isReferral=
{
isReferral
}
/>
<
CongratsStepContent
isReferral=
{
isReferral
}
customReferralReward=
{
customReferralReward
}
/>
}
</
ModalBody
>
</
ModalContent
>
...
...
ui/rewards/login/steps/CongratsStepContent.tsx
View file @
7b45a38a
...
...
@@ -12,14 +12,17 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
type
Props
=
{
isReferral
:
boolean
;
customReferralReward
:
string
|
null
;
};
const
CongratsStepContent
=
({
isReferral
}:
Props
)
=>
{
const
CongratsStepContent
=
({
isReferral
,
customReferralReward
}:
Props
)
=>
{
const
{
referralsQuery
,
rewardsConfigQuery
}
=
useRewardsContext
();
const
registrationReward
=
rewardsConfigQuery
.
data
?.
rewards
.
registration
;
const
registrationWithReferralReward
=
rewardsConfigQuery
.
data
?.
rewards
.
registration_with_referral
;
const
referralReward
=
Number
(
registrationWithReferralReward
)
-
Number
(
registrationReward
);
const
registrationReward
=
Number
(
rewardsConfigQuery
.
data
?.
rewards
.
registration
);
const
registrationWithReferralReward
=
customReferralReward
?
Number
(
customReferralReward
)
+
registrationReward
:
Number
(
rewardsConfigQuery
.
data
?.
rewards
.
registration_with_referral
);
const
referralReward
=
registrationWithReferralReward
-
registrationReward
;
const
refLink
=
referralsQuery
.
data
?.
link
||
'
N/A
'
;
const
shareText
=
`I joined the @blockscout Merits Program and got my first
${
registrationReward
||
'
N/A
'
}
#Merits! Use this link for a sign-up bonus and start earning rewards with @blockscout block explorer.\n\n
${
refLink
}
`
;
// eslint-disable-line max-len
...
...
@@ -41,7 +44,7 @@ const CongratsStepContent = ({ isReferral }: Props) => {
<
MeritsIcon
boxSize=
{
{
base
:
isReferral
?
8
:
12
,
md
:
12
}
}
mr=
{
{
base
:
isReferral
?
1
:
2
,
md
:
2
}
}
/>
<
Skeleton
isLoaded=
{
!
rewardsConfigQuery
.
isLoading
}
>
<
Text
fontSize=
{
{
base
:
isReferral
?
'
24px
'
:
'
30px
'
,
md
:
'
30px
'
}
}
fontWeight=
"700"
color=
{
textColor
}
>
+
{
rewardsConfigQuery
.
data
?.
rewards
[
isReferral
?
'
registration_with_referral
'
:
'
registration
'
]
||
'
N/A
'
}
+
{
(
isReferral
?
registrationWithReferralReward
:
registrationReward
)
||
'
N/A
'
}
</
Text
>
</
Skeleton
>
{
isReferral
&&
(
...
...
ui/rewards/login/steps/LoginStepContent.tsx
View file @
7b45a38a
...
...
@@ -13,7 +13,7 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
import
useProfileQuery
from
'
ui/snippets/auth/useProfileQuery
'
;
type
Props
=
{
goNext
:
(
isReferral
:
boolean
)
=>
void
;
goNext
:
(
isReferral
:
boolean
,
reward
:
string
|
null
)
=>
void
;
closeModal
:
()
=>
void
;
openAuthModal
:
(
isAuth
:
boolean
,
trySharedLogin
?:
boolean
)
=>
void
;
};
...
...
@@ -23,9 +23,9 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const
{
connect
,
isConnected
,
address
}
=
useWallet
({
source
:
'
Merits
'
});
const
savedRefCode
=
cookies
.
get
(
cookies
.
NAMES
.
REWARDS_REFERRAL_CODE
);
const
[
isRefCodeUsed
,
setIsRefCodeUsed
]
=
useBoolean
(
Boolean
(
savedRefCode
));
const
[
isLoading
,
setIsLoading
]
=
use
Boolean
(
false
);
const
[
isLoading
,
setIsLoading
]
=
use
State
(
false
);
const
[
refCode
,
setRefCode
]
=
useState
(
savedRefCode
||
''
);
const
[
refCodeError
,
setRefCodeError
]
=
use
Boolean
(
false
);
const
[
refCodeError
,
setRefCodeError
]
=
use
State
(
false
);
const
{
login
,
checkUserQuery
,
rewardsConfigQuery
}
=
useRewardsContext
();
const
profileQuery
=
useProfileQuery
();
...
...
@@ -51,30 +51,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const
loginToRewardsProgram
=
useCallback
(
async
()
=>
{
try
{
setRefCodeError
.
off
(
);
setIsLoading
.
on
(
);
const
{
isNewUser
,
invalidRefCodeError
}
=
await
login
(
isSignUp
&&
isRefCodeUsed
?
refCode
:
''
);
setRefCodeError
(
false
);
setIsLoading
(
true
);
const
{
isNewUser
,
reward
,
invalidRefCodeError
}
=
await
login
(
isSignUp
&&
isRefCodeUsed
?
refCode
:
''
);
if
(
invalidRefCodeError
)
{
setRefCodeError
.
on
(
);
setRefCodeError
(
true
);
}
else
{
if
(
isNewUser
)
{
goNext
(
isRefCodeUsed
);
goNext
(
isRefCodeUsed
,
reward
);
}
else
{
closeModal
();
router
.
push
({
pathname
:
'
/account/merits
'
},
undefined
,
{
shallow
:
true
});
}
}
}
catch
(
error
)
{}
setIsLoading
.
off
(
);
},
[
login
,
goNext
,
setIsLoading
,
router
,
closeModal
,
refCode
,
setRefCodeError
,
isRefCodeUsed
,
isSignUp
]);
setIsLoading
(
false
);
},
[
login
,
goNext
,
router
,
closeModal
,
refCode
,
isRefCodeUsed
,
isSignUp
]);
useEffect
(()
=>
{
if
(
isSignUp
&&
isRefCodeUsed
&&
refCode
.
length
>
0
&&
refCode
.
length
!==
6
)
{
setRefCodeError
.
on
();
}
else
{
setRefCodeError
.
off
();
}
},
[
refCode
,
isRefCodeUsed
,
isSignUp
]);
// eslint-disable-line react-hooks/exhaustive-deps
const
isInvalid
=
isSignUp
&&
isRefCodeUsed
&&
refCode
.
length
>
0
&&
refCode
.
length
!==
6
&&
refCode
.
length
!==
12
;
setRefCodeError
(
isInvalid
);
},
[
refCode
,
isRefCodeUsed
,
isSignUp
]);
const
handleButtonClick
=
React
.
useCallback
(()
=>
{
if
(
canTrySharedLogin
)
{
...
...
@@ -145,7 +142,10 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
<
FormInputPlaceholder
text=
"Code"
/>
</
FormControl
>
<
Text
fontSize=
"sm"
variant=
"secondary"
mt=
{
1
}
color=
{
refCodeError
?
'
red.500
'
:
undefined
}
>
{
refCodeError
?
'
Incorrect code or format
'
:
'
The code should be in format XXXXXX
'
}
{
refCodeError
?
'
Incorrect code or format (6 or 12 characters)
'
:
'
The code should be in format XXXXXX
'
}
</
Text
>
</>
)
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment