@zerodev/wallet-react-ui 0.0.6 → 0.0.8
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 +69 -10
- package/dist/_types/auth/authStoreSlice.d.ts +3 -4
- package/dist/_types/auth/authStoreSlice.d.ts.map +1 -1
- package/dist/_types/auth/components/WalletGridSheet/index.d.ts +15 -0
- package/dist/_types/auth/components/WalletGridSheet/index.d.ts.map +1 -0
- 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/InstalledWallets.d.ts +12 -0
- package/dist/_types/auth/pages/SignUp/InstalledWallets.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/MoreWallets.d.ts +5 -0
- package/dist/_types/auth/pages/SignUp/MoreWallets.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/Wallet.d.ts +7 -0
- package/dist/_types/auth/pages/SignUp/Wallet.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts +30 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/index.d.ts +37 -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 +2 -10
- 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/auth/walletGuide.d.ts +108 -0
- package/dist/_types/auth/walletGuide.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 +5 -3
- package/dist/_types/index.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 +810 -576
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/auth/authStoreSlice.ts +7 -13
- package/src/auth/components/WalletGridSheet/index.tsx +77 -0
- package/src/auth/hooks/useAuth.ts +0 -5
- package/src/auth/index.tsx +19 -20
- 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/InstalledWallets.tsx +95 -0
- package/src/auth/pages/SignUp/MoreWallets.tsx +105 -0
- package/src/auth/pages/SignUp/Passkey.tsx +56 -0
- package/src/auth/pages/SignUp/Wallet.tsx +81 -0
- package/src/auth/pages/SignUp/context.tsx +46 -0
- package/src/auth/pages/SignUp/index.tsx +164 -0
- package/src/auth/pages/Verifying.tsx +3 -10
- package/src/auth/types.ts +1 -11
- package/src/auth/utils/isCancellationError.ts +10 -0
- package/src/auth/walletGuide.ts +132 -0
- package/src/connector.ts +12 -28
- package/src/index.ts +4 -3
- 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/dist/_types/auth/pages/WalletSelection.d.ts +0 -2
- package/dist/_types/auth/pages/WalletSelection.d.ts.map +0 -1
- package/src/auth/pages/SignUp.tsx +0 -311
- package/src/auth/pages/WalletSelection.tsx +0 -61
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ListItem, ListItemChevron, ListItemIcon } from '@zerodev/react-ui'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { useConnect, useConnectors } from 'wagmi'
|
|
4
|
+
import {
|
|
5
|
+
WalletGridSheet,
|
|
6
|
+
type WalletTileData,
|
|
7
|
+
} from '../../components/WalletGridSheet'
|
|
8
|
+
import { useAuth } from '../../hooks/useAuth'
|
|
9
|
+
import { isCancellationError } from '../../utils/isCancellationError'
|
|
10
|
+
import { matchesWallet, WALLET_GUIDE } from '../../walletGuide'
|
|
11
|
+
import { useReportPending, useSignUpContext } from './context'
|
|
12
|
+
|
|
13
|
+
/** "More wallets" row — opens the wallet grid sheet. */
|
|
14
|
+
export function SignUpMoreWallets({
|
|
15
|
+
title = 'More wallets',
|
|
16
|
+
}: {
|
|
17
|
+
title?: string
|
|
18
|
+
}) {
|
|
19
|
+
const { goToStep } = useAuth()
|
|
20
|
+
const { authPending, guardAgreement, setError } = useSignUpContext()
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
const connectors = useConnectors()
|
|
23
|
+
const { mutate: connect, isPending } = useConnect()
|
|
24
|
+
useReportPending(isPending)
|
|
25
|
+
|
|
26
|
+
// Our own connector is the embedded wallet, and walletConnect-type
|
|
27
|
+
// connectors are the mobile transport rather than a wallet — neither
|
|
28
|
+
// belongs in the grid.
|
|
29
|
+
const walletConnectors = connectors.filter(
|
|
30
|
+
(c) => c.id !== 'zerodev-wallet' && c.type !== 'walletConnect',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const startConnect = (connector: (typeof connectors)[number]) => {
|
|
34
|
+
if (authPending) return
|
|
35
|
+
if (!guardAgreement()) return
|
|
36
|
+
setOpen(false)
|
|
37
|
+
setError(null)
|
|
38
|
+
connect(
|
|
39
|
+
{ connector },
|
|
40
|
+
{
|
|
41
|
+
// The external wallet is now the active wagmi connection — the
|
|
42
|
+
// embedded-wallet flow is done, so close it.
|
|
43
|
+
onSuccess: () => goToStep(null),
|
|
44
|
+
onError: (err) => {
|
|
45
|
+
if (!isCancellationError(err)) {
|
|
46
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Claimed by a connector → connect it; otherwise the tile opens the
|
|
54
|
+
// vendor download page.
|
|
55
|
+
const guideTiles: WalletTileData[] = WALLET_GUIDE.map((wallet) => {
|
|
56
|
+
const installed = walletConnectors.find((c) => matchesWallet(c, wallet))
|
|
57
|
+
return {
|
|
58
|
+
key: wallet.id,
|
|
59
|
+
name: wallet.name,
|
|
60
|
+
icon: wallet.icon,
|
|
61
|
+
onSelect: () => {
|
|
62
|
+
if (installed) {
|
|
63
|
+
startConnect(installed)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
setOpen(false)
|
|
67
|
+
window.open(wallet.downloadUrl, '_blank', 'noopener,noreferrer')
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Live connectors we have no guide entry for still get a tile — nothing
|
|
73
|
+
// installed is ever hidden.
|
|
74
|
+
const connectorTiles: WalletTileData[] = walletConnectors
|
|
75
|
+
.filter((c) => !WALLET_GUIDE.some((wallet) => matchesWallet(c, wallet)))
|
|
76
|
+
.map((connector) => ({
|
|
77
|
+
key: connector.uid,
|
|
78
|
+
name: connector.name,
|
|
79
|
+
icon: connector.icon,
|
|
80
|
+
onSelect: () => startConnect(connector),
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const handleClick = () => {
|
|
84
|
+
if (authPending) return
|
|
85
|
+
if (!guardAgreement()) return
|
|
86
|
+
setOpen(true)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<ListItem
|
|
92
|
+
icon={<ListItemIcon name="walletOutline" className="zd:w-5 zd:h-5" />}
|
|
93
|
+
title={title}
|
|
94
|
+
trailing={<ListItemChevron />}
|
|
95
|
+
disabled={authPending}
|
|
96
|
+
onClick={handleClick}
|
|
97
|
+
/>
|
|
98
|
+
<WalletGridSheet
|
|
99
|
+
open={open}
|
|
100
|
+
onOpenChange={setOpen}
|
|
101
|
+
tiles={[...guideTiles, ...connectorTiles]}
|
|
102
|
+
/>
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -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,81 @@
|
|
|
1
|
+
import { Badge, ListItem, ListItemChevron } from '@zerodev/react-ui'
|
|
2
|
+
import { useLayoutEffect } from 'react'
|
|
3
|
+
import { useConnect, useConnectors } from 'wagmi'
|
|
4
|
+
import { useAuth } from '../../hooks/useAuth'
|
|
5
|
+
import { isCancellationError } from '../../utils/isCancellationError'
|
|
6
|
+
import { matchesWallet, WALLET_GUIDE, type WalletId } from '../../walletGuide'
|
|
7
|
+
import { useReportPending, useSignUpContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Dedicated row for a single guide wallet: connects it directly when a live
|
|
10
|
+
* connector claims it, and is a download link for the vendor page otherwise. */
|
|
11
|
+
export function SignUpWallet({ walletId }: { walletId: WalletId }) {
|
|
12
|
+
const wallet = WALLET_GUIDE.find((w) => w.id === walletId)
|
|
13
|
+
if (!wallet) {
|
|
14
|
+
// Unreachable for TS consumers; catches raw-JS typos.
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Unknown walletId "${walletId}". Valid ids: ${WALLET_GUIDE.map((w) => w.id).join(', ')}`,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { goToStep } = useAuth()
|
|
21
|
+
const { authPending, guardAgreement, setError, registerWallet } =
|
|
22
|
+
useSignUpContext()
|
|
23
|
+
const connectors = useConnectors()
|
|
24
|
+
const { mutate: connect, isPending } = useConnect()
|
|
25
|
+
useReportPending(isPending)
|
|
26
|
+
useLayoutEffect(() => registerWallet(wallet.id), [registerWallet, wallet.id])
|
|
27
|
+
|
|
28
|
+
// Same claim rule as the MoreWallets grid: a 6963 announcement or a
|
|
29
|
+
// configured SDK connector both make the row connectable. Only an
|
|
30
|
+
// announcement (id === rdns) proves a live extension and earns the badge.
|
|
31
|
+
const installed = connectors.find((c) => matchesWallet(c, wallet))
|
|
32
|
+
const isAnnounced =
|
|
33
|
+
!!wallet.rdns && connectors.some((c) => c.id === wallet.rdns)
|
|
34
|
+
|
|
35
|
+
const shared = {
|
|
36
|
+
title: wallet.name,
|
|
37
|
+
icon: <img src={wallet.icon} alt="" className="zd:w-6 zd:h-6" />,
|
|
38
|
+
trailing: <ListItemChevron />,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!installed) {
|
|
42
|
+
return (
|
|
43
|
+
<ListItem {...shared} asChild>
|
|
44
|
+
{/* biome-ignore lint/a11y/useAnchorContent: the row layout (incl. the title text) is injected into the anchor via Slot */}
|
|
45
|
+
<a
|
|
46
|
+
href={wallet.downloadUrl}
|
|
47
|
+
target="_blank"
|
|
48
|
+
rel="noopener noreferrer"
|
|
49
|
+
/>
|
|
50
|
+
</ListItem>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleClick = () => {
|
|
55
|
+
if (authPending) return
|
|
56
|
+
if (!guardAgreement()) return
|
|
57
|
+
setError(null)
|
|
58
|
+
connect(
|
|
59
|
+
{ connector: installed },
|
|
60
|
+
{
|
|
61
|
+
// The external wallet is now the active wagmi connection — the
|
|
62
|
+
// embedded-wallet flow is done, so close it (mirrors SignUp.MoreWallets).
|
|
63
|
+
onSuccess: () => goToStep(null),
|
|
64
|
+
onError: (err) => {
|
|
65
|
+
if (!isCancellationError(err)) {
|
|
66
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ListItem
|
|
75
|
+
{...shared}
|
|
76
|
+
{...(isAnnounced && { subtitle: <Badge text="INSTALLED" /> })}
|
|
77
|
+
disabled={authPending}
|
|
78
|
+
onClick={handleClick}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
/** Guide ids of every mounted `SignUp.Wallet` pin. Discovery units skip
|
|
20
|
+
* these so a pinned wallet isn't listed twice. */
|
|
21
|
+
registeredWallets: readonly string[]
|
|
22
|
+
/** Returns the unregister function. */
|
|
23
|
+
registerWallet: (walletId: string) => () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SignUpContext = createContext<SignUpContextValue | null>(null)
|
|
27
|
+
|
|
28
|
+
export function useSignUpContext(): SignUpContextValue {
|
|
29
|
+
const ctx = useContext(SignUpContext)
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
throw new Error('SignUp.* components must be rendered inside <SignUp>')
|
|
32
|
+
}
|
|
33
|
+
return ctx
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Mirror a method's in-flight state into the shared pending flag.
|
|
37
|
+
* Clears on unmount so a removed unit can't leave the page locked.
|
|
38
|
+
* One flag, not per-unit: methods are mutually exclusive, so at most one
|
|
39
|
+
* unit reports `true` at a time. */
|
|
40
|
+
export function useReportPending(pending: boolean) {
|
|
41
|
+
const { setAuthPending } = useSignUpContext()
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setAuthPending(pending)
|
|
44
|
+
return () => setAuthPending(false)
|
|
45
|
+
}, [pending, setAuthPending])
|
|
46
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Button, Text } from '@zerodev/react-ui'
|
|
2
|
+
import { type ReactNode, useCallback, 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 { SignUpInstalledWallets } from './InstalledWallets'
|
|
10
|
+
import { SignUpMoreWallets } from './MoreWallets'
|
|
11
|
+
import { SignUpPasskey } from './Passkey'
|
|
12
|
+
import { SignUpWallet } from './Wallet'
|
|
13
|
+
|
|
14
|
+
type SignUpRootProps = {
|
|
15
|
+
children: ReactNode
|
|
16
|
+
/** Enable the consent gate: linked from the footer checkbox, and every
|
|
17
|
+
* method is blocked until the user agrees when either URL is set. */
|
|
18
|
+
termsAndConditionsUrl?: string | undefined
|
|
19
|
+
privacyPolicyUrl?: string | undefined
|
|
20
|
+
/** Which email verification flow the Email unit runs. */
|
|
21
|
+
emailAuthMethod?: EmailAuthMethod | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function SignUpRoot({
|
|
25
|
+
children,
|
|
26
|
+
termsAndConditionsUrl,
|
|
27
|
+
privacyPolicyUrl,
|
|
28
|
+
emailAuthMethod = 'magicLink',
|
|
29
|
+
}: SignUpRootProps) {
|
|
30
|
+
const [agreedToTerms, setAgreedToTerms] = useState(false)
|
|
31
|
+
const [highlightAgreement, setHighlightAgreement] = useState(false)
|
|
32
|
+
const [error, setError] = useState<string | null>(null)
|
|
33
|
+
// One flag for the whole page: methods are mutually exclusive, so at most
|
|
34
|
+
// one unit is in flight at a time. Known accepted edge: a unit unmounting
|
|
35
|
+
// mid-flight (conditional composition) clears a sibling's lock.
|
|
36
|
+
const [authPending, setAuthPending] = useState(false)
|
|
37
|
+
// Which wallets have a pinned `SignUp.Wallet` row mounted. A plain list of
|
|
38
|
+
// registrants: register appends, cleanup removes one occurrence.
|
|
39
|
+
const [registeredWallets, setRegisteredWallets] = useState<readonly string[]>(
|
|
40
|
+
[],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const registerWallet = useCallback((walletId: string) => {
|
|
44
|
+
setRegisteredWallets((prev) => [...prev, walletId])
|
|
45
|
+
return () => {
|
|
46
|
+
setRegisteredWallets((prev) => {
|
|
47
|
+
const next = [...prev]
|
|
48
|
+
next.splice(next.indexOf(walletId), 1)
|
|
49
|
+
return next
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const requiresAgreement = !!(termsAndConditionsUrl || privacyPolicyUrl)
|
|
55
|
+
const needsAgreement = requiresAgreement && !agreedToTerms
|
|
56
|
+
|
|
57
|
+
const guardAgreement = () => {
|
|
58
|
+
if (!needsAgreement) return true
|
|
59
|
+
setHighlightAgreement(true)
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<SignUpContext.Provider
|
|
65
|
+
value={{
|
|
66
|
+
authPending,
|
|
67
|
+
emailAuthMethod,
|
|
68
|
+
setAuthPending,
|
|
69
|
+
needsAgreement,
|
|
70
|
+
guardAgreement,
|
|
71
|
+
setError,
|
|
72
|
+
registeredWallets,
|
|
73
|
+
registerWallet,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{error !== null && (
|
|
77
|
+
<div className="zd:flex zd:items-center zd:justify-center zd:h-full zd:min-h-100">
|
|
78
|
+
<div className="zd:flex zd:flex-col zd:gap-4 zd:max-w-md">
|
|
79
|
+
<Text className="zd:text-h2 zd:text-center">Error occurred</Text>
|
|
80
|
+
<Text className="zd:text-center zd:text-red-500">{error}</Text>
|
|
81
|
+
<Button
|
|
82
|
+
action="primary"
|
|
83
|
+
text="Try again"
|
|
84
|
+
onClick={() => setError(null)}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{/* Kept mounted (hidden) during the error takeover so unit state — the
|
|
90
|
+
typed email, pending flags — survives "Try again".
|
|
91
|
+
`-mx-4` cancels Screen's scroll-container padding so this page
|
|
92
|
+
owns its own; inner blocks re-add `zd:px-4` where they need it. */}
|
|
93
|
+
<div
|
|
94
|
+
className={`zd:-mx-4 zd:flex-1 zd:flex zd:flex-col zd:justify-between zd:pb-4 zd:overflow-y-auto zd:overflow-x-hidden${
|
|
95
|
+
error !== null ? ' zd:hidden' : ''
|
|
96
|
+
}`}
|
|
97
|
+
>
|
|
98
|
+
<div className="zd:flex-1 zd:flex zd:flex-col zd:justify-center">
|
|
99
|
+
<div className="zd:px-4 zd:flex zd:flex-col zd:items-center">
|
|
100
|
+
<div className="zd:w-full zd:px-16 zd:py-4">
|
|
101
|
+
<BlobAnimation className="zd:w-full zd:pointer-events-none zd:select-none" />
|
|
102
|
+
</div>
|
|
103
|
+
<Text className="zd:text-h2 zd:text-center">
|
|
104
|
+
Continue to your wallet
|
|
105
|
+
</Text>
|
|
106
|
+
<Text className="zd:mt-2 zd:text-center zd:text-greyScale/50">
|
|
107
|
+
Choose a sign-in method to proceed
|
|
108
|
+
</Text>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="zd:mt-6 zd:mb-4 zd:px-4 zd:flex zd:flex-col zd:gap-2">
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="zd:px-4">
|
|
115
|
+
<SignUpFooter
|
|
116
|
+
termsAndConditionsUrl={termsAndConditionsUrl}
|
|
117
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
118
|
+
agreedToTerms={agreedToTerms}
|
|
119
|
+
setAgreedToTerms={(agreed) => {
|
|
120
|
+
setAgreedToTerms(agreed)
|
|
121
|
+
if (agreed) setHighlightAgreement(false)
|
|
122
|
+
}}
|
|
123
|
+
highlight={highlightAgreement}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</SignUpContext.Provider>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function SignUpDivider({ label = 'or' }: { label?: string }) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="zd:-mx-4 zd:my-2 zd:flex zd:items-center zd:gap-3">
|
|
134
|
+
<div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
|
|
135
|
+
<Text className="zd:text-body3">{label}</Text>
|
|
136
|
+
<div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The canonical sign-up page. Takes the root's own props (the consent-gate
|
|
142
|
+
* URLs) and forwards them — per-unit config (e.g. the email method) still
|
|
143
|
+
* means composing the units yourself. */
|
|
144
|
+
function SignUpDefault(props: Omit<SignUpRootProps, 'children'>) {
|
|
145
|
+
return (
|
|
146
|
+
<SignUpRoot {...props}>
|
|
147
|
+
<SignUpPasskey />
|
|
148
|
+
<SignUpDivider />
|
|
149
|
+
<SignUpGoogle />
|
|
150
|
+
<SignUpEmail />
|
|
151
|
+
</SignUpRoot>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const SignUp = Object.assign(SignUpRoot, {
|
|
156
|
+
Default: SignUpDefault,
|
|
157
|
+
Passkey: SignUpPasskey,
|
|
158
|
+
Google: SignUpGoogle,
|
|
159
|
+
Email: SignUpEmail,
|
|
160
|
+
Wallet: SignUpWallet,
|
|
161
|
+
InstalledWallets: SignUpInstalledWallets,
|
|
162
|
+
MoreWallets: SignUpMoreWallets,
|
|
163
|
+
Divider: SignUpDivider,
|
|
164
|
+
})
|
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthMethod = 'email' | 'google' | 'passkey'
|
|
1
|
+
export type AuthMethod = 'email' | 'google' | 'passkey' | 'external-wallet'
|
|
2
2
|
|
|
3
3
|
export type AuthStep =
|
|
4
4
|
| 'sign-up'
|
|
@@ -7,17 +7,7 @@ export type AuthStep =
|
|
|
7
7
|
| 'verifying-otp'
|
|
8
8
|
| 'passkey-prompt'
|
|
9
9
|
| 'oauth-in-progress'
|
|
10
|
-
| 'wallet-selection'
|
|
11
10
|
| 'authenticated'
|
|
12
11
|
| 'error'
|
|
13
12
|
|
|
14
13
|
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,10 @@
|
|
|
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
|
+
// EIP-1193 user rejection (external-wallet connect), via viem
|
|
6
|
+
if (err.name === 'UserRejectedRequestError') return true
|
|
7
|
+
// OAuth (existing logic, message-based)
|
|
8
|
+
const msg = err.message.toLowerCase()
|
|
9
|
+
return msg.includes('oauth popup was closed')
|
|
10
|
+
}
|