@zerodev/wallet-react-ui 0.0.5 → 0.0.7
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.
- package/README.md +54 -10
- package/dist/_types/auth/authStoreSlice.d.ts +3 -4
- package/dist/_types/auth/authStoreSlice.d.ts.map +1 -1
- package/dist/_types/auth/hooks/useAuth.d.ts +0 -2
- package/dist/_types/auth/hooks/useAuth.d.ts.map +1 -1
- package/dist/_types/auth/index.d.ts +9 -1
- package/dist/_types/auth/index.d.ts.map +1 -1
- package/dist/_types/auth/pages/OtpInput.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/Email.d.ts +4 -0
- package/dist/_types/auth/pages/SignUp/Email.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/Google.d.ts +3 -0
- package/dist/_types/auth/pages/SignUp/Google.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/Passkey.d.ts +3 -0
- package/dist/_types/auth/pages/SignUp/Passkey.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts +25 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/index.d.ts +31 -0
- package/dist/_types/auth/pages/SignUp/index.d.ts.map +1 -0
- package/dist/_types/auth/pages/Verifying.d.ts.map +1 -1
- package/dist/_types/auth/types.d.ts +0 -8
- package/dist/_types/auth/types.d.ts.map +1 -1
- package/dist/_types/auth/utils/isCancellationError.d.ts +2 -0
- package/dist/_types/auth/utils/isCancellationError.d.ts.map +1 -0
- package/dist/_types/connector.d.ts +3 -13
- package/dist/_types/connector.d.ts.map +1 -1
- package/dist/_types/index.d.ts +4 -3
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/_types/shared/utils/common.d.ts +0 -1
- package/dist/_types/shared/utils/common.d.ts.map +1 -1
- package/dist/_types/store.d.ts +1 -6
- package/dist/_types/store.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +555 -562
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/auth/authStoreSlice.ts +7 -13
- package/src/auth/hooks/useAuth.ts +0 -5
- package/src/auth/index.tsx +19 -7
- package/src/auth/pages/OtpInput.tsx +1 -4
- package/src/auth/pages/SignUp/Email.tsx +86 -0
- package/src/auth/pages/SignUp/Google.tsx +43 -0
- package/src/auth/pages/SignUp/Passkey.tsx +56 -0
- package/src/auth/pages/SignUp/context.tsx +41 -0
- package/src/auth/pages/SignUp/index.tsx +138 -0
- package/src/auth/pages/Verifying.tsx +3 -10
- package/src/auth/types.ts +0 -9
- package/src/auth/utils/isCancellationError.ts +8 -0
- package/src/connector.ts +12 -28
- package/src/index.ts +3 -3
- package/src/shared/utils/common.ts +0 -12
- package/src/store.ts +1 -8
- package/dist/_types/auth/pages/SignUp.d.ts +0 -2
- package/dist/_types/auth/pages/SignUp.d.ts.map +0 -1
- package/src/auth/pages/SignUp.tsx +0 -311
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StateCreator } from 'zustand'
|
|
2
|
-
import type {
|
|
2
|
+
import type { AuthStep } from './types'
|
|
3
3
|
|
|
4
4
|
const OTP_SESSION_STORAGE_KEY = 'zerodev:auth:otpSession'
|
|
5
5
|
|
|
@@ -47,7 +47,6 @@ export interface AuthStoreSlice {
|
|
|
47
47
|
// State
|
|
48
48
|
step: AuthStep | null
|
|
49
49
|
stepHistory: AuthStep[]
|
|
50
|
-
enabledMethods: AuthMethod[]
|
|
51
50
|
email: string | null
|
|
52
51
|
setEmail: (email: string) => void
|
|
53
52
|
otpId: string | null
|
|
@@ -63,10 +62,10 @@ export interface AuthStoreSlice {
|
|
|
63
62
|
}) => void
|
|
64
63
|
/** Clear the persisted OTP session after a successful verify. */
|
|
65
64
|
clearOtpSession: () => void
|
|
66
|
-
config: AuthConfig | null
|
|
67
65
|
|
|
68
66
|
// Actions
|
|
69
|
-
|
|
67
|
+
/** Restore a persisted OTP session (survives reloads mid-email-flow). */
|
|
68
|
+
initialize: () => void
|
|
70
69
|
goToStep: (step: AuthStep | null) => void
|
|
71
70
|
goBack: () => void
|
|
72
71
|
reset: () => void
|
|
@@ -83,24 +82,19 @@ export const createAuthStoreSlice: StateCreator<
|
|
|
83
82
|
// Initial state
|
|
84
83
|
step: null,
|
|
85
84
|
stepHistory: [],
|
|
86
|
-
enabledMethods: [],
|
|
87
85
|
email: null,
|
|
88
86
|
otpId: null,
|
|
89
87
|
otpEncryptionTargetBundle: null,
|
|
90
|
-
config: null,
|
|
91
88
|
|
|
92
89
|
// Actions
|
|
93
|
-
initialize: (
|
|
90
|
+
initialize: () => {
|
|
94
91
|
const stored = readStoredOtpSession()
|
|
92
|
+
if (!stored) return
|
|
95
93
|
set((state) => ({
|
|
96
94
|
auth: {
|
|
97
95
|
...state.auth,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
...(stored && {
|
|
101
|
-
otpId: stored.otpId,
|
|
102
|
-
otpEncryptionTargetBundle: stored.otpEncryptionTargetBundle,
|
|
103
|
-
}),
|
|
96
|
+
otpId: stored.otpId,
|
|
97
|
+
otpEncryptionTargetBundle: stored.otpEncryptionTargetBundle,
|
|
104
98
|
},
|
|
105
99
|
}))
|
|
106
100
|
},
|
|
@@ -12,16 +12,11 @@ export function useAuth() {
|
|
|
12
12
|
store,
|
|
13
13
|
(state) => state.auth.otpEncryptionTargetBundle,
|
|
14
14
|
)
|
|
15
|
-
const enabledMethods = useStore(store, (state) => state.auth.enabledMethods)
|
|
16
|
-
const authConfig = useStore(store, (state) => state.auth.config)
|
|
17
|
-
|
|
18
15
|
return {
|
|
19
16
|
step,
|
|
20
17
|
email,
|
|
21
18
|
otpId,
|
|
22
19
|
otpEncryptionTargetBundle,
|
|
23
|
-
enabledMethods,
|
|
24
|
-
config: authConfig,
|
|
25
20
|
goToStep: store.getState().auth.goToStep,
|
|
26
21
|
goBack: stepHistory.length > 0 ? store.getState().auth.goBack : null,
|
|
27
22
|
reset: store.getState().auth.reset,
|
package/src/auth/index.tsx
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Screen, TopNav } from '@zerodev/react-ui'
|
|
2
2
|
import { type ReactNode, useEffect } from 'react'
|
|
3
|
-
import { useStore } from 'zustand'
|
|
4
3
|
import { StatusScreen } from '../shared/components/StatusScreen'
|
|
5
|
-
import { useKitStore } from '../shared/hooks/useKitStore'
|
|
6
4
|
import { useAuth } from './hooks/useAuth'
|
|
7
5
|
import { EmailVerification } from './pages/EmailVerification'
|
|
8
6
|
import { ErrorScreen } from './pages/ErrorScreen'
|
|
@@ -37,10 +35,13 @@ function PasskeyPrompt() {
|
|
|
37
35
|
)
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
function renderStep(
|
|
38
|
+
function renderStep(
|
|
39
|
+
step: AuthStep | null,
|
|
40
|
+
renderSignUp?: (() => ReactNode) | undefined,
|
|
41
|
+
): ReactNode {
|
|
41
42
|
switch (step) {
|
|
42
43
|
case 'sign-up':
|
|
43
|
-
return <SignUp />
|
|
44
|
+
return renderSignUp ? renderSignUp() : <SignUp.Default />
|
|
44
45
|
case 'email-verification':
|
|
45
46
|
return <EmailVerification />
|
|
46
47
|
case 'otp-input':
|
|
@@ -60,15 +61,23 @@ function renderStep(step: AuthStep | null): ReactNode {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export function
|
|
64
|
+
export function ConnectWallet({
|
|
64
65
|
onClose: userOnClose,
|
|
65
66
|
size,
|
|
67
|
+
renderSignUp,
|
|
68
|
+
logo,
|
|
66
69
|
}: {
|
|
67
70
|
onClose?: (() => void) | undefined
|
|
68
71
|
size?: 'sm' | 'md' | 'lg' | undefined
|
|
72
|
+
/** Replace the default sign-up page: compose `SignUp.*` units inside
|
|
73
|
+
* `<SignUp>`. Omit to render the canonical page (`SignUp.Default`). */
|
|
74
|
+
renderSignUp?: (() => ReactNode) | undefined
|
|
75
|
+
/** Optional brand logo for the top nav on the sign-up page. When omitted,
|
|
76
|
+
* no logo is shown. `PoweredBy` always shows the ZeroDev mark
|
|
77
|
+
* independently. */
|
|
78
|
+
logo?: ReactNode | undefined
|
|
69
79
|
} = {}) {
|
|
70
80
|
const { step, goToStep, goBack, reset } = useAuth()
|
|
71
|
-
const logo = useStore(useKitStore(), (s) => s.logo)
|
|
72
81
|
|
|
73
82
|
useEffect(() => {
|
|
74
83
|
if (step === null && hasMagicLinkCodeInUrl()) {
|
|
@@ -76,7 +85,7 @@ export function AuthFlow({
|
|
|
76
85
|
}
|
|
77
86
|
}, [step, goToStep])
|
|
78
87
|
|
|
79
|
-
const content = renderStep(step)
|
|
88
|
+
const content = renderStep(step, renderSignUp)
|
|
80
89
|
if (!content) return null
|
|
81
90
|
|
|
82
91
|
const handleClose = () => {
|
|
@@ -89,6 +98,9 @@ export function AuthFlow({
|
|
|
89
98
|
return (
|
|
90
99
|
<Screen
|
|
91
100
|
{...(size && { size })}
|
|
101
|
+
// Sign-up shrinks to its content (few methods → shorter card) but never
|
|
102
|
+
// grows past the standard height; other steps keep the fixed size.
|
|
103
|
+
className={step === 'sign-up' ? 'zd:h-auto zd:max-h-202.5' : undefined}
|
|
92
104
|
// Some elements in SignUp need to go from edge to edge.
|
|
93
105
|
// No vertical padding; we set px-0 so we can fully control this padding.
|
|
94
106
|
contentClassName={step === 'sign-up' ? 'zd:px-0' : undefined}
|
|
@@ -12,7 +12,6 @@ export function OtpInput() {
|
|
|
12
12
|
setOtpSession,
|
|
13
13
|
clearOtpSession,
|
|
14
14
|
goToStep,
|
|
15
|
-
config,
|
|
16
15
|
} = useAuth()
|
|
17
16
|
const { mutateAsync: sendOtp, isPending: isSendOtpPending } = useSendOTP()
|
|
18
17
|
const { mutateAsync: verifyOtp, isPending } = useVerifyOTP()
|
|
@@ -43,10 +42,8 @@ export function OtpInput() {
|
|
|
43
42
|
})
|
|
44
43
|
clearOtpSession()
|
|
45
44
|
goToStep('authenticated')
|
|
46
|
-
|
|
47
|
-
} catch (err) {
|
|
45
|
+
} catch {
|
|
48
46
|
setError(true)
|
|
49
|
-
config?.onError?.(err)
|
|
50
47
|
}
|
|
51
48
|
}
|
|
52
49
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Icon, Input } from '@zerodev/react-ui'
|
|
2
|
+
import { useSendMagicLink, useSendOTP } from '@zerodev/wallet-react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { isValidEmailAddress } from '../../../shared/utils/common'
|
|
5
|
+
import { useAuth } from '../../hooks/useAuth'
|
|
6
|
+
import { useReportPending, useSignUpContext } from './context'
|
|
7
|
+
|
|
8
|
+
/** Email input row: sends an OTP or magic link depending on the root's
|
|
9
|
+
* `emailAuthMethod`, then advances to the matching verification step. */
|
|
10
|
+
export function SignUpEmail() {
|
|
11
|
+
const { goToStep, setEmail, setOtpSession } = useAuth()
|
|
12
|
+
const {
|
|
13
|
+
authPending,
|
|
14
|
+
emailAuthMethod,
|
|
15
|
+
needsAgreement,
|
|
16
|
+
guardAgreement,
|
|
17
|
+
setError,
|
|
18
|
+
} = useSignUpContext()
|
|
19
|
+
const [emailInput, setEmailInput] = useState('')
|
|
20
|
+
|
|
21
|
+
const shouldUseOtp = emailAuthMethod === 'otp'
|
|
22
|
+
const { mutateAsync: sendOtp, isPending: isSendOtpPending } = useSendOTP()
|
|
23
|
+
const { mutateAsync: sendMagicLink, isPending: isSendMagicLinkPending } =
|
|
24
|
+
useSendMagicLink()
|
|
25
|
+
const isEmailLoading = isSendOtpPending || isSendMagicLinkPending
|
|
26
|
+
useReportPending(isEmailLoading)
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async () => {
|
|
29
|
+
if (!emailInput || authPending) return
|
|
30
|
+
if (!isValidEmailAddress(emailInput)) return
|
|
31
|
+
if (!guardAgreement()) return
|
|
32
|
+
|
|
33
|
+
setError(null)
|
|
34
|
+
try {
|
|
35
|
+
const send = shouldUseOtp ? sendOtp : sendMagicLink
|
|
36
|
+
const { otpId, otpEncryptionTargetBundle } = await send({
|
|
37
|
+
email: emailInput,
|
|
38
|
+
})
|
|
39
|
+
setEmail(emailInput)
|
|
40
|
+
setOtpSession({ otpId, otpEncryptionTargetBundle })
|
|
41
|
+
goToStep(shouldUseOtp ? 'otp-input' : 'email-verification')
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setError(
|
|
44
|
+
err instanceof Error ? err.message : 'Failed to send verification code',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Input
|
|
51
|
+
iconName="email"
|
|
52
|
+
placeholder="Enter your email"
|
|
53
|
+
value={emailInput}
|
|
54
|
+
onChange={(e) => setEmailInput(e.target.value)}
|
|
55
|
+
type="email"
|
|
56
|
+
autoCapitalize="none"
|
|
57
|
+
autoComplete="email"
|
|
58
|
+
disabled={authPending}
|
|
59
|
+
variant="listItemStyle"
|
|
60
|
+
onKeyDown={(e) => {
|
|
61
|
+
if (e.key === 'Enter' && emailInput && !authPending) {
|
|
62
|
+
handleSubmit()
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{isEmailLoading ? (
|
|
67
|
+
<div className="zd:w-13 zd:h-13 zd:flex zd:items-center zd:justify-center">
|
|
68
|
+
<div className="zd:w-5 zd:h-5 zd:border-2 zd:border-solarOrange zd:border-t-transparent zd:rounded-full zd:animate-spin" />
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
disabled={!isValidEmailAddress(emailInput) || needsAgreement}
|
|
74
|
+
className={`zd:w-13 zd:h-13 zd:rounded-2xl zd:flex zd:items-center zd:justify-center zd:transition-colors ${
|
|
75
|
+
isValidEmailAddress(emailInput) && !needsAgreement
|
|
76
|
+
? 'zd:cursor-pointer'
|
|
77
|
+
: 'zd:cursor-not-allowed zd:opacity-50'
|
|
78
|
+
}`}
|
|
79
|
+
onClick={() => handleSubmit()}
|
|
80
|
+
>
|
|
81
|
+
<Icon name="chevronRight" className="zd:text-greyScale" />
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</Input>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ListItem, ListItemChevron, ListItemIcon } from '@zerodev/react-ui'
|
|
2
|
+
import { useAuthenticateOAuth } from '@zerodev/wallet-react'
|
|
3
|
+
import { useAuth } from '../../hooks/useAuth'
|
|
4
|
+
import { isCancellationError } from '../../utils/isCancellationError'
|
|
5
|
+
import { useReportPending, useSignUpContext } from './context'
|
|
6
|
+
|
|
7
|
+
/** "Google" OAuth row. */
|
|
8
|
+
export function SignUpGoogle() {
|
|
9
|
+
const { goToStep } = useAuth()
|
|
10
|
+
const { authPending, guardAgreement, setError } = useSignUpContext()
|
|
11
|
+
|
|
12
|
+
const { mutateAsync: authenticateOAuth, isPending } = useAuthenticateOAuth({
|
|
13
|
+
mutation: {
|
|
14
|
+
onSuccess: () => {
|
|
15
|
+
goToStep('authenticated')
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
useReportPending(isPending)
|
|
20
|
+
|
|
21
|
+
const handleClick = async () => {
|
|
22
|
+
if (authPending) return
|
|
23
|
+
if (!guardAgreement()) return
|
|
24
|
+
setError(null)
|
|
25
|
+
try {
|
|
26
|
+
await authenticateOAuth({ provider: 'google' })
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (!isCancellationError(err)) {
|
|
29
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ListItem
|
|
36
|
+
icon={<ListItemIcon name="google" />}
|
|
37
|
+
title="Google"
|
|
38
|
+
trailing={<ListItemChevron />}
|
|
39
|
+
disabled={authPending}
|
|
40
|
+
onClick={handleClick}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Button } from '@zerodev/react-ui'
|
|
2
|
+
import { useLoginPasskey, useRegisterPasskey } from '@zerodev/wallet-react'
|
|
3
|
+
import { useAuth } from '../../hooks/useAuth'
|
|
4
|
+
import { isCancellationError } from '../../utils/isCancellationError'
|
|
5
|
+
import { useReportPending, useSignUpContext } from './context'
|
|
6
|
+
|
|
7
|
+
/** "Create a passkey" + "Log in with passkey" buttons. */
|
|
8
|
+
export function SignUpPasskey() {
|
|
9
|
+
const { goToStep } = useAuth()
|
|
10
|
+
const { authPending, guardAgreement, setError } = useSignUpContext()
|
|
11
|
+
|
|
12
|
+
const mutation = {
|
|
13
|
+
onSuccess: () => {
|
|
14
|
+
goToStep('authenticated')
|
|
15
|
+
},
|
|
16
|
+
onError: (err: unknown) => {
|
|
17
|
+
if (!isCancellationError(err)) {
|
|
18
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
const { mutate: registerPasskey, isPending: isRegisterPending } =
|
|
23
|
+
useRegisterPasskey({ mutation })
|
|
24
|
+
const { mutate: loginPasskey, isPending: isLoginPending } = useLoginPasskey({
|
|
25
|
+
mutation,
|
|
26
|
+
})
|
|
27
|
+
useReportPending(isRegisterPending || isLoginPending)
|
|
28
|
+
|
|
29
|
+
const guarded = (run: () => void) => () => {
|
|
30
|
+
if (authPending) return
|
|
31
|
+
if (!guardAgreement()) return
|
|
32
|
+
setError(null)
|
|
33
|
+
run()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Button
|
|
39
|
+
action="secondary"
|
|
40
|
+
text="Create a passkey"
|
|
41
|
+
iconName="key"
|
|
42
|
+
trailIcon
|
|
43
|
+
disabled={authPending}
|
|
44
|
+
onClick={guarded(() => registerPasskey())}
|
|
45
|
+
/>
|
|
46
|
+
<Button
|
|
47
|
+
action="secondary"
|
|
48
|
+
text="Log in with passkey"
|
|
49
|
+
iconName="key"
|
|
50
|
+
trailIcon
|
|
51
|
+
disabled={authPending}
|
|
52
|
+
onClick={guarded(() => loginPasskey())}
|
|
53
|
+
/>
|
|
54
|
+
</>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect } from 'react'
|
|
2
|
+
import type { EmailAuthMethod } from '../../types'
|
|
3
|
+
|
|
4
|
+
export type SignUpContextValue = {
|
|
5
|
+
/** True while any method's auth attempt is in flight — used to disable
|
|
6
|
+
* sibling methods so two flows can't run at once. */
|
|
7
|
+
authPending: boolean
|
|
8
|
+
setAuthPending: (pending: boolean) => void
|
|
9
|
+
/** Which email verification flow the Email unit runs. Set on the root
|
|
10
|
+
* (`<SignUp emailAuthMethod=…>`); already resolved to its default here. */
|
|
11
|
+
emailAuthMethod: EmailAuthMethod
|
|
12
|
+
/** True when the terms checkbox is required but unchecked. For passive
|
|
13
|
+
* disabled styling; use `guardAgreement` before starting an attempt. */
|
|
14
|
+
needsAgreement: boolean
|
|
15
|
+
/** Call before starting an auth attempt: highlights the terms checkbox and
|
|
16
|
+
* returns false when agreement is required but missing. */
|
|
17
|
+
guardAgreement: () => boolean
|
|
18
|
+
setError: (message: string | null) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SignUpContext = createContext<SignUpContextValue | null>(null)
|
|
22
|
+
|
|
23
|
+
export function useSignUpContext(): SignUpContextValue {
|
|
24
|
+
const ctx = useContext(SignUpContext)
|
|
25
|
+
if (!ctx) {
|
|
26
|
+
throw new Error('SignUp.* components must be rendered inside <SignUp>')
|
|
27
|
+
}
|
|
28
|
+
return ctx
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mirror a method's in-flight state into the shared pending flag.
|
|
32
|
+
* Clears on unmount so a removed unit can't leave the page locked.
|
|
33
|
+
* One flag, not per-unit: methods are mutually exclusive, so at most one
|
|
34
|
+
* unit reports `true` at a time. */
|
|
35
|
+
export function useReportPending(pending: boolean) {
|
|
36
|
+
const { setAuthPending } = useSignUpContext()
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setAuthPending(pending)
|
|
39
|
+
return () => setAuthPending(false)
|
|
40
|
+
}, [pending, setAuthPending])
|
|
41
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Button, Text } from '@zerodev/react-ui'
|
|
2
|
+
import { type ReactNode, useState } from 'react'
|
|
3
|
+
import { SignUpFooter } from '../../../shared/components/SignUpFooter'
|
|
4
|
+
import { BlobAnimation } from '../../components/BlobAnimation'
|
|
5
|
+
import type { EmailAuthMethod } from '../../types'
|
|
6
|
+
import { SignUpContext } from './context'
|
|
7
|
+
import { SignUpEmail } from './Email'
|
|
8
|
+
import { SignUpGoogle } from './Google'
|
|
9
|
+
import { SignUpPasskey } from './Passkey'
|
|
10
|
+
|
|
11
|
+
type SignUpRootProps = {
|
|
12
|
+
children: ReactNode
|
|
13
|
+
/** Enable the consent gate: linked from the footer checkbox, and every
|
|
14
|
+
* method is blocked until the user agrees when either URL is set. */
|
|
15
|
+
termsAndConditionsUrl?: string | undefined
|
|
16
|
+
privacyPolicyUrl?: string | undefined
|
|
17
|
+
/** Which email verification flow the Email unit runs. */
|
|
18
|
+
emailAuthMethod?: EmailAuthMethod | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function SignUpRoot({
|
|
22
|
+
children,
|
|
23
|
+
termsAndConditionsUrl,
|
|
24
|
+
privacyPolicyUrl,
|
|
25
|
+
emailAuthMethod = 'magicLink',
|
|
26
|
+
}: SignUpRootProps) {
|
|
27
|
+
const [agreedToTerms, setAgreedToTerms] = useState(false)
|
|
28
|
+
const [highlightAgreement, setHighlightAgreement] = useState(false)
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
// One flag for the whole page: methods are mutually exclusive, so at most
|
|
31
|
+
// one unit is in flight at a time. Known accepted edge: a unit unmounting
|
|
32
|
+
// mid-flight (conditional composition) clears a sibling's lock.
|
|
33
|
+
const [authPending, setAuthPending] = useState(false)
|
|
34
|
+
|
|
35
|
+
const requiresAgreement = !!(termsAndConditionsUrl || privacyPolicyUrl)
|
|
36
|
+
const needsAgreement = requiresAgreement && !agreedToTerms
|
|
37
|
+
|
|
38
|
+
const guardAgreement = () => {
|
|
39
|
+
if (!needsAgreement) return true
|
|
40
|
+
setHighlightAgreement(true)
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SignUpContext.Provider
|
|
46
|
+
value={{
|
|
47
|
+
authPending,
|
|
48
|
+
emailAuthMethod,
|
|
49
|
+
setAuthPending,
|
|
50
|
+
needsAgreement,
|
|
51
|
+
guardAgreement,
|
|
52
|
+
setError,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{error !== null && (
|
|
56
|
+
<div className="zd:flex zd:items-center zd:justify-center zd:h-full">
|
|
57
|
+
<div className="zd:flex zd:flex-col zd:gap-4 zd:max-w-md">
|
|
58
|
+
<Text className="zd:text-h2 zd:text-center">Error occurred</Text>
|
|
59
|
+
<Text className="zd:text-center zd:text-red-500">{error}</Text>
|
|
60
|
+
<Button
|
|
61
|
+
action="primary"
|
|
62
|
+
text="Try again"
|
|
63
|
+
onClick={() => setError(null)}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
{/* Kept mounted (hidden) during the error takeover so unit state — the
|
|
69
|
+
typed email, pending flags — survives "Try again". */}
|
|
70
|
+
<div
|
|
71
|
+
className={`zd:flex-1 zd:flex zd:flex-col zd:justify-between zd:pb-4 zd:overflow-y-auto zd:overflow-x-hidden${
|
|
72
|
+
error !== null ? ' zd:hidden' : ''
|
|
73
|
+
}`}
|
|
74
|
+
>
|
|
75
|
+
<div className="zd:flex-1 zd:flex zd:flex-col zd:justify-center">
|
|
76
|
+
<div className="zd:px-4 zd:flex zd:flex-col zd:items-center">
|
|
77
|
+
<div className="zd:w-full zd:px-16 zd:py-4">
|
|
78
|
+
<BlobAnimation className="zd:w-full zd:pointer-events-none zd:select-none" />
|
|
79
|
+
</div>
|
|
80
|
+
<Text className="zd:text-h2 zd:text-center">
|
|
81
|
+
Continue to your wallet
|
|
82
|
+
</Text>
|
|
83
|
+
<Text className="zd:mt-2 zd:text-center zd:text-greyScale/50">
|
|
84
|
+
Choose a sign-in method to proceed
|
|
85
|
+
</Text>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="zd:mt-6 zd:mb-4 zd:px-4 zd:flex zd:flex-col zd:gap-2">
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="zd:px-4">
|
|
92
|
+
<SignUpFooter
|
|
93
|
+
termsAndConditionsUrl={termsAndConditionsUrl}
|
|
94
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
95
|
+
agreedToTerms={agreedToTerms}
|
|
96
|
+
setAgreedToTerms={(agreed) => {
|
|
97
|
+
setAgreedToTerms(agreed)
|
|
98
|
+
if (agreed) setHighlightAgreement(false)
|
|
99
|
+
}}
|
|
100
|
+
highlight={highlightAgreement}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</SignUpContext.Provider>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SignUpDivider({ label = 'or' }: { label?: string }) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="zd:-mx-4 zd:my-2 zd:flex zd:items-center zd:gap-3">
|
|
111
|
+
<div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
|
|
112
|
+
<Text className="zd:text-body3">{label}</Text>
|
|
113
|
+
<div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The canonical sign-up page. Takes the root's own props (the consent-gate
|
|
119
|
+
* URLs) and forwards them — per-unit config (e.g. the email method) still
|
|
120
|
+
* means composing the units yourself. */
|
|
121
|
+
function SignUpDefault(props: Omit<SignUpRootProps, 'children'>) {
|
|
122
|
+
return (
|
|
123
|
+
<SignUpRoot {...props}>
|
|
124
|
+
<SignUpPasskey />
|
|
125
|
+
<SignUpDivider />
|
|
126
|
+
<SignUpGoogle />
|
|
127
|
+
<SignUpEmail />
|
|
128
|
+
</SignUpRoot>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const SignUp = Object.assign(SignUpRoot, {
|
|
133
|
+
Default: SignUpDefault,
|
|
134
|
+
Passkey: SignUpPasskey,
|
|
135
|
+
Google: SignUpGoogle,
|
|
136
|
+
Email: SignUpEmail,
|
|
137
|
+
Divider: SignUpDivider,
|
|
138
|
+
})
|
|
@@ -11,13 +11,8 @@ function getCodeFromUrl(): string | null {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function Verifying() {
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
otpEncryptionTargetBundle,
|
|
17
|
-
goToStep,
|
|
18
|
-
clearOtpSession,
|
|
19
|
-
config,
|
|
20
|
-
} = useAuth()
|
|
14
|
+
const { otpId, otpEncryptionTargetBundle, goToStep, clearOtpSession } =
|
|
15
|
+
useAuth()
|
|
21
16
|
const [code] = useState<string | null>(getCodeFromUrl)
|
|
22
17
|
const [error, setError] = useState<Error | null>(null)
|
|
23
18
|
|
|
@@ -26,14 +21,12 @@ export function Verifying() {
|
|
|
26
21
|
const { mutate: verifyMagicLink, isPending: isVerificationLoading } =
|
|
27
22
|
useVerifyMagicLink({
|
|
28
23
|
mutation: {
|
|
29
|
-
onSuccess:
|
|
24
|
+
onSuccess: () => {
|
|
30
25
|
clearOtpSession()
|
|
31
26
|
goToStep('authenticated')
|
|
32
|
-
config?.onSuccess?.()
|
|
33
27
|
},
|
|
34
28
|
onError: (err) => {
|
|
35
29
|
setError(err)
|
|
36
|
-
config?.onError?.(err)
|
|
37
30
|
},
|
|
38
31
|
},
|
|
39
32
|
})
|
package/src/auth/types.ts
CHANGED
|
@@ -12,12 +12,3 @@ export type AuthStep =
|
|
|
12
12
|
| 'error'
|
|
13
13
|
|
|
14
14
|
export type EmailAuthMethod = 'magicLink' | 'otp'
|
|
15
|
-
|
|
16
|
-
export interface AuthConfig {
|
|
17
|
-
enabledMethods: AuthMethod[]
|
|
18
|
-
emailAuthMethod?: EmailAuthMethod
|
|
19
|
-
termsAndConditionsUrl?: string
|
|
20
|
-
privacyPolicyUrl?: string
|
|
21
|
-
onSuccess?: () => void
|
|
22
|
-
onError?: (error: unknown) => void
|
|
23
|
-
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function isCancellationError(err: unknown): boolean {
|
|
2
|
+
if (!(err instanceof Error)) return false
|
|
3
|
+
// WebAuthn / passkey
|
|
4
|
+
if (err.name === 'AbortError' || err.name === 'NotAllowedError') return true
|
|
5
|
+
// OAuth (existing logic, message-based)
|
|
6
|
+
const msg = err.message.toLowerCase()
|
|
7
|
+
return msg.includes('oauth popup was closed')
|
|
8
|
+
}
|
package/src/connector.ts
CHANGED
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
zeroDevWallet as baseZeroDevWallet,
|
|
8
8
|
NotAuthenticatedError,
|
|
9
9
|
} from '@zerodev/wallet-react'
|
|
10
|
-
import type { ReactNode } from 'react'
|
|
11
|
-
import type { AuthConfig } from './auth/types'
|
|
12
10
|
import { createStore } from './store.js'
|
|
13
11
|
import type { Request, RequestMethod } from './types.js'
|
|
14
12
|
|
|
@@ -20,18 +18,9 @@ const DEFAULT_SIGNING_PROMPT_METHODS: RequestMethod[] = [
|
|
|
20
18
|
'eth_signTypedData_v4',
|
|
21
19
|
]
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* Optional brand logo rendered in the auth flow's top nav. When omitted,
|
|
27
|
-
* no logo is shown. `PoweredBy` always shows the ZeroDev mark independently.
|
|
28
|
-
*/
|
|
29
|
-
logo?: ReactNode
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type ZeroDevKitConnectorParams = ZeroDevWalletConnectorParams & {
|
|
33
|
-
config?: ZeroDevKitConfig
|
|
34
|
-
}
|
|
21
|
+
/** The kit connector takes exactly the base connector's param
|
|
22
|
+
We alias it to leave room for expansion */
|
|
23
|
+
export type ZeroDevKitConnectorParams = ZeroDevWalletConnectorParams
|
|
35
24
|
|
|
36
25
|
function requireUserConfirmation(
|
|
37
26
|
store: ReturnType<typeof createStore>,
|
|
@@ -74,12 +63,7 @@ export function zeroDevWallet(
|
|
|
74
63
|
params: ZeroDevKitConnectorParams,
|
|
75
64
|
): CreateConnectorFn {
|
|
76
65
|
const baseFactory = baseZeroDevWallet(params)
|
|
77
|
-
const store = createStore(
|
|
78
|
-
|
|
79
|
-
// Initialize auth config if provided
|
|
80
|
-
if (params.config?.auth) {
|
|
81
|
-
store.getState().auth.initialize(params.config.auth)
|
|
82
|
-
}
|
|
66
|
+
const store = createStore()
|
|
83
67
|
|
|
84
68
|
return (wagmiConfig) => {
|
|
85
69
|
const connector = baseFactory(wagmiConfig)
|
|
@@ -101,7 +85,6 @@ export function zeroDevWallet(
|
|
|
101
85
|
} catch (error) {
|
|
102
86
|
if (
|
|
103
87
|
connectParams?.isReconnecting ||
|
|
104
|
-
!params.config?.auth ||
|
|
105
88
|
!(error instanceof NotAuthenticatedError)
|
|
106
89
|
) {
|
|
107
90
|
throw error
|
|
@@ -119,24 +102,25 @@ export function zeroDevWallet(
|
|
|
119
102
|
|
|
120
103
|
async disconnect() {
|
|
121
104
|
await connector.disconnect?.()
|
|
122
|
-
|
|
123
|
-
store.getState().auth.reset()
|
|
124
|
-
}
|
|
105
|
+
store.getState().auth.reset()
|
|
125
106
|
},
|
|
126
107
|
|
|
127
108
|
async setup() {
|
|
128
109
|
await connector.setup?.()
|
|
129
110
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
111
|
+
// Everything below is browser-only. Skip during SSR — getProvider()
|
|
112
|
+
// touches `window` for EIP-6963 discovery and would crash on the
|
|
113
|
+
// server, and the session restore reads localStorage.
|
|
133
114
|
if (typeof window === 'undefined') return
|
|
134
115
|
|
|
116
|
+
// Restore a persisted OTP session so an email flow survives a reload.
|
|
117
|
+
store.getState().auth.initialize()
|
|
118
|
+
|
|
135
119
|
// Signing is pinned to background mode: the prompt-mode confirmation UI
|
|
136
120
|
// is not part of this package's public surface, so requests always pass
|
|
137
121
|
// through without gating. The wrapping logic below is retained but
|
|
138
122
|
// unreachable.
|
|
139
|
-
// const signing = params.
|
|
123
|
+
// const signing = params.signing
|
|
140
124
|
const signing = { mode: 'background' } as const
|
|
141
125
|
if (signing.mode === 'background') return
|
|
142
126
|
|