@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.
Files changed (56) hide show
  1. package/README.md +54 -10
  2. package/dist/_types/auth/authStoreSlice.d.ts +3 -4
  3. package/dist/_types/auth/authStoreSlice.d.ts.map +1 -1
  4. package/dist/_types/auth/hooks/useAuth.d.ts +0 -2
  5. package/dist/_types/auth/hooks/useAuth.d.ts.map +1 -1
  6. package/dist/_types/auth/index.d.ts +9 -1
  7. package/dist/_types/auth/index.d.ts.map +1 -1
  8. package/dist/_types/auth/pages/OtpInput.d.ts.map +1 -1
  9. package/dist/_types/auth/pages/SignUp/Email.d.ts +4 -0
  10. package/dist/_types/auth/pages/SignUp/Email.d.ts.map +1 -0
  11. package/dist/_types/auth/pages/SignUp/Google.d.ts +3 -0
  12. package/dist/_types/auth/pages/SignUp/Google.d.ts.map +1 -0
  13. package/dist/_types/auth/pages/SignUp/Passkey.d.ts +3 -0
  14. package/dist/_types/auth/pages/SignUp/Passkey.d.ts.map +1 -0
  15. package/dist/_types/auth/pages/SignUp/context.d.ts +25 -0
  16. package/dist/_types/auth/pages/SignUp/context.d.ts.map +1 -0
  17. package/dist/_types/auth/pages/SignUp/index.d.ts +31 -0
  18. package/dist/_types/auth/pages/SignUp/index.d.ts.map +1 -0
  19. package/dist/_types/auth/pages/Verifying.d.ts.map +1 -1
  20. package/dist/_types/auth/types.d.ts +0 -8
  21. package/dist/_types/auth/types.d.ts.map +1 -1
  22. package/dist/_types/auth/utils/isCancellationError.d.ts +2 -0
  23. package/dist/_types/auth/utils/isCancellationError.d.ts.map +1 -0
  24. package/dist/_types/connector.d.ts +3 -13
  25. package/dist/_types/connector.d.ts.map +1 -1
  26. package/dist/_types/index.d.ts +4 -3
  27. package/dist/_types/index.d.ts.map +1 -1
  28. package/dist/_types/shared/utils/common.d.ts +0 -1
  29. package/dist/_types/shared/utils/common.d.ts.map +1 -1
  30. package/dist/_types/store.d.ts +1 -6
  31. package/dist/_types/store.d.ts.map +1 -1
  32. package/dist/index.cjs +3 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.mjs +555 -562
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/styles.css +1 -1
  37. package/package.json +4 -4
  38. package/src/auth/authStoreSlice.ts +7 -13
  39. package/src/auth/hooks/useAuth.ts +0 -5
  40. package/src/auth/index.tsx +19 -7
  41. package/src/auth/pages/OtpInput.tsx +1 -4
  42. package/src/auth/pages/SignUp/Email.tsx +86 -0
  43. package/src/auth/pages/SignUp/Google.tsx +43 -0
  44. package/src/auth/pages/SignUp/Passkey.tsx +56 -0
  45. package/src/auth/pages/SignUp/context.tsx +41 -0
  46. package/src/auth/pages/SignUp/index.tsx +138 -0
  47. package/src/auth/pages/Verifying.tsx +3 -10
  48. package/src/auth/types.ts +0 -9
  49. package/src/auth/utils/isCancellationError.ts +8 -0
  50. package/src/connector.ts +12 -28
  51. package/src/index.ts +3 -3
  52. package/src/shared/utils/common.ts +0 -12
  53. package/src/store.ts +1 -8
  54. package/dist/_types/auth/pages/SignUp.d.ts +0 -2
  55. package/dist/_types/auth/pages/SignUp.d.ts.map +0 -1
  56. package/src/auth/pages/SignUp.tsx +0 -311
@@ -1,5 +1,5 @@
1
1
  import type { StateCreator } from 'zustand'
2
- import type { AuthConfig, AuthMethod, AuthStep } from './types'
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
- initialize: (config: AuthConfig) => void
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: (config: AuthConfig) => {
90
+ initialize: () => {
94
91
  const stored = readStoredOtpSession()
92
+ if (!stored) return
95
93
  set((state) => ({
96
94
  auth: {
97
95
  ...state.auth,
98
- config,
99
- enabledMethods: config.enabledMethods,
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,
@@ -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(step: AuthStep | null): ReactNode {
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 AuthFlow({
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
- config?.onSuccess?.()
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
- otpId,
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: async () => {
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
- export type ZeroDevKitConfig = {
24
- auth?: AuthConfig
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({ logo: params.config?.logo })
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
- if (params.config?.auth) {
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
- // Request-wrapping is only meaningful in the browser (it gates calls
131
- // on UI confirmation). Skip during SSR getProvider() touches
132
- // `window` for EIP-6963 discovery and would crash on the server.
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.config?.signing
123
+ // const signing = params.signing
140
124
  const signing = { mode: 'background' } as const
141
125
  if (signing.mode === 'background') return
142
126