11import {
22 AgreeToTermsPolicy ,
3+ experience ,
34 ExtraParamsKey ,
45 InteractionEvent ,
56 SignInIdentifier ,
67 type RequestErrorBody ,
78} from '@logto/schemas' ;
8- import { condString } from '@silverhand/essentials' ;
9- import { useCallback , useContext , useEffect , useState } from 'react' ;
10- import { useSearchParams } from 'react-router-dom' ;
9+ import { useCallback , useContext , useEffect , useRef , useState } from 'react' ;
10+ import { useNavigate , useSearchParams } from 'react-router-dom' ;
1111
1212import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext' ;
1313import {
1414 identifyAndSubmitInteraction ,
15- signInWithVerifiedIdentifier ,
16- registerWithOneTimeToken ,
15+ registerWithVerifiedIdentifier ,
16+ signInWithOneTimeToken ,
1717} from '@/apis/experience' ;
1818import LoadingLayer from '@/components/LoadingLayer' ;
1919import useApi from '@/hooks/use-api' ;
@@ -22,15 +22,16 @@ import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
2222import useSubmitInteractionErrorHandler from '@/hooks/use-submit-interaction-error-handler' ;
2323import useTerms from '@/hooks/use-terms' ;
2424
25- import ErrorPage from '../ErrorPage' ;
26-
2725const OneTimeToken = ( ) => {
2826 const [ params ] = useSearchParams ( ) ;
29- const [ oneTimeTokenError , setOneTimeTokenError ] = useState < string | boolean > ( ) ;
27+ const navigate = useNavigate ( ) ;
28+ const [ isLoading , setIsLoading ] = useState ( false ) ;
29+ const hasTermsAgreed = useRef ( false ) ;
30+ const isSubmitted = useRef ( false ) ;
3031
3132 const asyncIdentifyUserAndSubmit = useApi ( identifyAndSubmitInteraction ) ;
32- const asyncSignInWithVerifiedIdentifier = useApi ( signInWithVerifiedIdentifier ) ;
33- const asyncRegisterWithOneTimeToken = useApi ( registerWithOneTimeToken ) ;
33+ const asyncSignInWithOneTimeToken = useApi ( signInWithOneTimeToken ) ;
34+ const asyncRegisterWithVerifiedIdentifier = useApi ( registerWithVerifiedIdentifier ) ;
3435
3536 const { setIdentifierInputValue } = useContext ( UserInteractionContext ) ;
3637 const { termsValidation, agreeToTermsPolicy } = useTerms ( ) ;
@@ -40,22 +41,43 @@ const OneTimeToken = () => {
4041 const preRegisterErrorHandler = useSubmitInteractionErrorHandler ( InteractionEvent . Register ) ;
4142
4243 /**
43- * Update interaction event to `SignIn `, and then identify user and submit.
44+ * Update interaction event to `Register `, and then identify user and submit.
4445 */
45- const signInWithOneTimeToken = useCallback (
46+ const registerWithOneTimeToken = useCallback (
4647 async ( verificationId : string ) => {
47- const [ error , result ] = await asyncSignInWithVerifiedIdentifier ( verificationId ) ;
48+ if (
49+ ! hasTermsAgreed . current &&
50+ agreeToTermsPolicy !== AgreeToTermsPolicy . Automatic &&
51+ ! ( await termsValidation ( ) )
52+ ) {
53+ navigate (
54+ { pathname : `/${ experience . routes . oneTimeToken } /error` } ,
55+ { replace : true , state : { errorMessage : 'terms_acceptance_required_description' } }
56+ ) ;
57+ return ;
58+ }
59+
60+ setIsLoading ( true ) ;
61+ const [ error , result ] = await asyncRegisterWithVerifiedIdentifier ( verificationId ) ;
4862
4963 if ( error ) {
50- await handleError ( error , preSignInErrorHandler ) ;
64+ await handleError ( error , preRegisterErrorHandler ) ;
5165 return ;
5266 }
5367
5468 if ( result ?. redirectTo ) {
5569 await redirectTo ( result . redirectTo ) ;
5670 }
5771 } ,
58- [ preSignInErrorHandler , asyncSignInWithVerifiedIdentifier , handleError , redirectTo ]
72+ [
73+ agreeToTermsPolicy ,
74+ preRegisterErrorHandler ,
75+ asyncRegisterWithVerifiedIdentifier ,
76+ navigate ,
77+ handleError ,
78+ redirectTo ,
79+ termsValidation ,
80+ ]
5981 ) ;
6082
6183 /**
@@ -67,11 +89,12 @@ const OneTimeToken = () => {
6789 const [ error , result ] = await asyncIdentifyUserAndSubmit ( { verificationId } ) ;
6890
6991 if ( error ) {
92+ setIsLoading ( false ) ;
7093 await handleError ( error , {
71- 'user.email_already_in_use ' : async ( ) => {
72- await signInWithOneTimeToken ( verificationId ) ;
94+ 'user.user_not_exist ' : async ( ) => {
95+ await registerWithOneTimeToken ( verificationId ) ;
7396 } ,
74- ...preRegisterErrorHandler ,
97+ ...preSignInErrorHandler ,
7598 } ) ;
7699 return ;
77100 }
@@ -81,53 +104,82 @@ const OneTimeToken = () => {
81104 }
82105 } ,
83106 [
84- preRegisterErrorHandler ,
107+ preSignInErrorHandler ,
85108 asyncIdentifyUserAndSubmit ,
86109 handleError ,
87110 redirectTo ,
88- signInWithOneTimeToken ,
111+ registerWithOneTimeToken ,
89112 ]
90113 ) ;
91114
115+ // Single effect: validate params, run terms gating once, then proceed to submission with idempotency
92116 useEffect ( ( ) => {
93117 ( async ( ) => {
94118 const token = params . get ( ExtraParamsKey . OneTimeToken ) ;
95119 const email = params . get ( ExtraParamsKey . LoginHint ) ;
96120 const errorMessage = params . get ( 'errorMessage' ) ;
97121
98122 if ( errorMessage ) {
99- setOneTimeTokenError ( errorMessage ) ;
123+ navigate (
124+ { pathname : `/${ experience . routes . oneTimeToken } /error` } ,
125+ { replace : true , state : { errorMessage } }
126+ ) ;
100127 return ;
101128 }
102129
103130 if ( ! token || ! email ) {
104- setOneTimeTokenError ( true ) ;
131+ navigate ( `/ ${ experience . routes . oneTimeToken } /error` , { replace : true } ) ;
105132 return ;
106133 }
107134
108- /**
109- * Check if the user has agreed to the terms and privacy policy before navigating to the 3rd-party social sign-in page
110- * when the policy is set to `Manual`
111- */
112- if ( agreeToTermsPolicy === AgreeToTermsPolicy . Manual && ! ( await termsValidation ( ) ) ) {
135+ if ( agreeToTermsPolicy === AgreeToTermsPolicy . Manual ) {
136+ const isAgreed = await termsValidation ( ) ;
137+
138+ // eslint-disable-next-line @silverhand/fp/no-mutation
139+ hasTermsAgreed . current = isAgreed ;
140+
141+ if ( ! isAgreed ) {
142+ navigate (
143+ { pathname : `/${ experience . routes . oneTimeToken } /error` } ,
144+ {
145+ replace : true ,
146+ state : {
147+ title : 'error.terms_acceptance_required' ,
148+ message : 'error.terms_acceptance_required_description' ,
149+ } ,
150+ }
151+ ) ;
152+ return ;
153+ }
154+ }
155+
156+ if ( isSubmitted . current ) {
113157 return ;
114158 }
159+ // eslint-disable-next-line @silverhand/fp/no-mutation
160+ isSubmitted . current ||= true ;
115161
116- const [ error , result ] = await asyncRegisterWithOneTimeToken ( {
162+ setIsLoading ( true ) ;
163+ const [ error , result ] = await asyncSignInWithOneTimeToken ( {
117164 token,
118165 identifier : { type : SignInIdentifier . Email , value : email } ,
119166 } ) ;
120167
121168 if ( error ) {
169+ setIsLoading ( false ) ;
122170 await handleError ( error , {
123171 global : ( error : RequestErrorBody ) => {
124- setOneTimeTokenError ( error . message ) ;
172+ navigate (
173+ { pathname : `/${ experience . routes . oneTimeToken } /error` } ,
174+ { replace : true , state : { errorMessage : error . message } }
175+ ) ;
125176 } ,
126177 } ) ;
127178 return ;
128179 }
129180
130181 if ( ! result ?. verificationId ) {
182+ setIsLoading ( false ) ;
131183 return ;
132184 }
133185
@@ -136,28 +188,19 @@ const OneTimeToken = () => {
136188 setIdentifierInputValue ( { type : SignInIdentifier . Email , value : email } ) ;
137189
138190 await submit ( result . verificationId ) ;
191+ setIsLoading ( false ) ;
139192 } ) ( ) ;
140193 } , [
141194 agreeToTermsPolicy ,
142195 params ,
143- asyncRegisterWithOneTimeToken ,
196+ asyncSignInWithOneTimeToken ,
144197 handleError ,
198+ navigate ,
145199 setIdentifierInputValue ,
146200 submit ,
147201 termsValidation ,
148202 ] ) ;
149203
150- if ( oneTimeTokenError ) {
151- return (
152- < ErrorPage
153- isNavbarHidden
154- title = "error.invalid_link"
155- message = "error.invalid_link_description"
156- rawMessage = { condString ( typeof oneTimeTokenError !== 'boolean' && oneTimeTokenError ) }
157- />
158- ) ;
159- }
160-
161- return < LoadingLayer /> ;
204+ return isLoading ? < LoadingLayer /> : null ;
162205} ;
163206export default OneTimeToken ;
0 commit comments