@luxfi/ui 5.5.4 → 5.5.5
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/components/auth/callback.tsx +40 -0
- package/components/auth/login-panel.tsx +15 -44
- package/components/auth/login.tsx +47 -0
- package/components/auth/provider.tsx +34 -0
- package/components/auth/resume.ts +16 -0
- package/components/auth/widget.tsx +80 -0
- package/components/chat-widget.tsx +6 -7
- package/components/commerce/checkout-panel/desktop-cp.tsx +2 -2
- package/components/commerce/checkout-panel/mobile-cp.tsx +2 -2
- package/components/footer.tsx +1 -1
- package/components/header/desktop.tsx +4 -4
- package/components/header/index.tsx +1 -1
- package/components/header/mobile-nav-menu-ai.tsx +1 -1
- package/components/header/mobile-nav-menu.tsx +2 -2
- package/components/header/mobile.tsx +3 -3
- package/components/icons/index.ts +0 -1
- package/components/index.ts +2 -2
- package/components/logo.tsx +39 -67
- package/components/security/index.tsx +55 -0
- package/next/middleware/determine-device-mw.ts +0 -13
- package/package.json +8 -7
- package/root-layout/index.tsx +3 -10
- package/style/lux-global.css +49 -0
- package/types/site-def.ts +7 -0
- package/components/auth/auth-listener.tsx +0 -33
- package/components/auth/auth-token/clear-auth-token.tsx +0 -12
- package/components/auth/auth-token/set-auth-token.tsx +0 -16
- package/components/auth/common-auth-domains.ts +0 -17
- package/components/auth/mobile-login-button.tsx +0 -112
- package/components/auth/signup-panel.tsx +0 -113
- package/components/icons/lux-logo.tsx +0 -10
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useIam } from '@hanzo/iam/react'
|
|
6
|
+
|
|
7
|
+
import { resume } from './resume'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The site's OAuth redirect target. IAM sends the authorization code here and
|
|
11
|
+
* this redeems it for tokens with the PKCE verifier the browser kept, then
|
|
12
|
+
* returns the visitor to the page they left.
|
|
13
|
+
*/
|
|
14
|
+
const Callback = () => {
|
|
15
|
+
|
|
16
|
+
const { handleCallback } = useIam()
|
|
17
|
+
const [error, setError] = useState<string | null>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
handleCallback()
|
|
21
|
+
.then(() => { window.location.replace(resume()) })
|
|
22
|
+
.catch((e: Error) => { setError(e.message) })
|
|
23
|
+
/* Runs once: the authorization code is single-use. */
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className='flex min-h-screen items-center justify-center p-8 text-center'>
|
|
28
|
+
{error ? (
|
|
29
|
+
<div className='flex flex-col gap-4'>
|
|
30
|
+
<p role='alert'>{error}</p>
|
|
31
|
+
<a href='/' className='underline'>Back to the site</a>
|
|
32
|
+
</div>
|
|
33
|
+
) : (
|
|
34
|
+
<p>Signing you in…</p>
|
|
35
|
+
)}
|
|
36
|
+
</main>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default Callback
|
|
@@ -1,55 +1,26 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import React from 'react'
|
|
3
4
|
import Link from 'next/link'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
5
|
-
import { setCookie } from 'cookies-next'
|
|
6
5
|
|
|
7
6
|
import { cn } from '@hanzo/ui/util'
|
|
8
7
|
import { Button, Carousel, CarouselContent, CarouselItem } from '@hanzo/ui/primitives'
|
|
9
|
-
import { LoginPanel as Login } from '@hanzo/auth/components'
|
|
10
8
|
|
|
11
|
-
import
|
|
9
|
+
import Login from './login'
|
|
10
|
+
import { LuxLogo } from '@luxfi/logo'
|
|
12
11
|
import Logo from '../logo'
|
|
13
12
|
import { EmblaAutoplay } from '..'
|
|
14
13
|
import { legal } from '../../site-def/footer'
|
|
15
14
|
|
|
16
15
|
const LoginPanel: React.FC<{
|
|
17
16
|
close: () => void
|
|
18
|
-
getStartedUrl?: string
|
|
19
|
-
redirectUrl?: string
|
|
20
17
|
className?: string
|
|
21
18
|
reviews: { text: string, author: string, href: string }[]
|
|
22
|
-
setIsLogin?: React.Dispatch<React.SetStateAction<boolean>>
|
|
23
19
|
}> = ({
|
|
24
20
|
close,
|
|
25
|
-
getStartedUrl = '/',
|
|
26
|
-
redirectUrl,
|
|
27
21
|
className = '',
|
|
28
|
-
reviews
|
|
29
|
-
setIsLogin
|
|
22
|
+
reviews
|
|
30
23
|
}) => {
|
|
31
|
-
const router = useRouter()
|
|
32
|
-
|
|
33
|
-
const termsOfServiceUrl = legal.find(({ title }) => title === 'Terms and Conditions')?.href || ''
|
|
34
|
-
const privacyPolicyUrl = legal.find(({ title }) => title === 'Privacy Policy')?.href || ''
|
|
35
|
-
const domains = ['lux.id', 'lux.credit', 'lux.market', 'lux.network', 'lux.shop', 'wallet.lux.network', 'safe.lux.network', 'lux.finance', 'lux.exchange', 'lux.quest']
|
|
36
|
-
// TODO :aa shouldn't this happen in @hanzo/auth: components/LoginPanel ??
|
|
37
|
-
// Otherwise, the functionality is split across modules! (and client/fw!)
|
|
38
|
-
// (This was never my intent w the onLoginChanged callback.)
|
|
39
|
-
const onLogin = (token: string) => {
|
|
40
|
-
for (let i = 0; i < domains.length; i ++) {
|
|
41
|
-
setCookie('auth-token', token, {
|
|
42
|
-
domain: domains[i],
|
|
43
|
-
path: '/',
|
|
44
|
-
sameSite: 'none',
|
|
45
|
-
secure: true,
|
|
46
|
-
httpOnly: false,
|
|
47
|
-
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
redirectUrl && router.push(redirectUrl)
|
|
52
|
-
}
|
|
53
24
|
|
|
54
25
|
return (
|
|
55
26
|
<div className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
|
|
@@ -61,7 +32,7 @@ const LoginPanel: React.FC<{
|
|
|
61
32
|
onClick={close}
|
|
62
33
|
className='w-fit !min-w-0 p-2'
|
|
63
34
|
>
|
|
64
|
-
<Logo size='md'
|
|
35
|
+
<Logo size='md' variant='wordmark' />
|
|
65
36
|
</Button>
|
|
66
37
|
<Carousel
|
|
67
38
|
options={{ align: 'center', loop: true }}
|
|
@@ -90,17 +61,17 @@ const LoginPanel: React.FC<{
|
|
|
90
61
|
onClick={close}
|
|
91
62
|
className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
|
|
92
63
|
>
|
|
93
|
-
<LuxLogo
|
|
64
|
+
<LuxLogo variant='white' size={20} />
|
|
94
65
|
</Button>
|
|
95
|
-
<Login
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
66
|
+
<Login className='w-full max-w-sm' />
|
|
67
|
+
<p className='text-sm opacity-50'>
|
|
68
|
+
{legal.map(({ title, href }, index) => (
|
|
69
|
+
<React.Fragment key={href}>
|
|
70
|
+
{index > 0 && ' · '}
|
|
71
|
+
<Link href={href} className='underline'>{title}</Link>
|
|
72
|
+
</React.Fragment>
|
|
73
|
+
))}
|
|
74
|
+
</p>
|
|
104
75
|
</div>
|
|
105
76
|
</div>
|
|
106
77
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@hanzo/ui/util'
|
|
6
|
+
import { Login as IamLogin, useIam } from '@hanzo/iam/react'
|
|
7
|
+
|
|
8
|
+
import { remember } from './resume'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign-in rendered in place, on this site's own domain: IAM mints an
|
|
12
|
+
* authorization code bound to a PKCE challenge and the form navigates to
|
|
13
|
+
* /auth/callback to redeem it. No credential ever reaches this bundle's
|
|
14
|
+
* origin, and there is one sign-in surface rather than one per app.
|
|
15
|
+
*/
|
|
16
|
+
const Login: React.FC<{
|
|
17
|
+
className?: string
|
|
18
|
+
onSuccess?: (token: string) => void
|
|
19
|
+
}> = ({
|
|
20
|
+
className,
|
|
21
|
+
onSuccess
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
const { config } = useIam()
|
|
25
|
+
/* OIDC anti-CSRF nonce, one per mounted form. */
|
|
26
|
+
const [state] = useState(() => crypto.randomUUID())
|
|
27
|
+
|
|
28
|
+
/* Sign-in leaves for /auth/callback, so record the page holding this form to
|
|
29
|
+
come back to. */
|
|
30
|
+
useEffect(remember, [])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<IamLogin
|
|
34
|
+
serverUrl={config.serverUrl}
|
|
35
|
+
clientId={config.clientId}
|
|
36
|
+
redirectUri={config.redirectUri}
|
|
37
|
+
organization={config.organization}
|
|
38
|
+
state={state}
|
|
39
|
+
/* Naming a class replaces the view's own, so keep both: the site skins
|
|
40
|
+
.hanzo-iam-* once, in lux-global.css. */
|
|
41
|
+
className={cn('hanzo-iam-card', className)}
|
|
42
|
+
onSuccess={onSuccess}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default Login
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { type PropsWithChildren } from 'react'
|
|
4
|
+
|
|
5
|
+
import { IamProvider } from '@hanzo/iam/react'
|
|
6
|
+
import { BRAND_SERVER_URLS } from '@hanzo/iam/paths'
|
|
7
|
+
import type { IAMConfig } from '@hanzo/iam/browser'
|
|
8
|
+
|
|
9
|
+
/* These are Lux sites, so they sign in at Lux's own issuer. Naming another
|
|
10
|
+
brand's would send a Lux user to a login page that has never heard of the
|
|
11
|
+
redirect_uri they came from. */
|
|
12
|
+
const ORG = 'lux'
|
|
13
|
+
|
|
14
|
+
/* Where the authorization code comes back. Every site mounts <Callback/> here. */
|
|
15
|
+
const CALLBACK = '/auth/callback'
|
|
16
|
+
|
|
17
|
+
/* The OAuth client a site presents itself as: the `<org>-<app>` pair registered
|
|
18
|
+
on lux.id, so lux.shop is `lux-shop`. */
|
|
19
|
+
const configure = (app: string): IAMConfig => ({
|
|
20
|
+
serverUrl: BRAND_SERVER_URLS[ORG],
|
|
21
|
+
organization: ORG,
|
|
22
|
+
clientId: `${ORG}-${app}`,
|
|
23
|
+
/* Read per render rather than at module load: one bundle serves the real
|
|
24
|
+
host and any preview of it, and each must redeem its code back to itself. */
|
|
25
|
+
redirectUri: (typeof window === 'undefined' ? '' : window.location.origin) + CALLBACK,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/* Provider is memoized on the config's values, not its identity, so building a
|
|
29
|
+
fresh object here does not rebuild the SDK. */
|
|
30
|
+
const Auth: React.FC<{ app: string } & PropsWithChildren> = ({ app, children }) => (
|
|
31
|
+
<IamProvider config={configure(app)}>{children}</IamProvider>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export default Auth
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* Where the visitor was when they chose to sign in. The OIDC round-trip lands
|
|
2
|
+
on /auth/callback, so without this every sign-in would end at the home page.
|
|
3
|
+
Session-scoped: it describes one trip through the browser, not a preference. */
|
|
4
|
+
const KEY = 'lux-auth-resume'
|
|
5
|
+
|
|
6
|
+
const remember = () => {
|
|
7
|
+
sessionStorage.setItem(KEY, window.location.pathname + window.location.search)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const resume = (): string => {
|
|
11
|
+
const to = sessionStorage.getItem(KEY)
|
|
12
|
+
sessionStorage.removeItem(KEY)
|
|
13
|
+
return to ?? '/'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { remember, resume }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useContext } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button, Popover, PopoverContent, PopoverTrigger, Separator } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
import { IamContext, useIam, useIamIdentity } from '@hanzo/iam/react'
|
|
8
|
+
|
|
9
|
+
import { remember } from './resume'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Hide the sign-in button, leaving only the profile of an already signed-in
|
|
13
|
+
user. Checkout wants this: it has its own sign-in step. */
|
|
14
|
+
noLogin?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
/** Sign in in place instead of leaving for lux.id — the mobile drawer opens
|
|
17
|
+
its own panel. */
|
|
18
|
+
handleLogin?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Widget: React.FC<Props> = ({
|
|
22
|
+
noLogin = false,
|
|
23
|
+
handleLogin,
|
|
24
|
+
className
|
|
25
|
+
}) => {
|
|
26
|
+
|
|
27
|
+
const { isAuthenticated, isLoading, login, logout } = useIam()
|
|
28
|
+
const identity = useIamIdentity()
|
|
29
|
+
|
|
30
|
+
/* Say nothing until the stored token has been read, rather than flashing
|
|
31
|
+
"Login" at someone who is already signed in. */
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!isAuthenticated) {
|
|
37
|
+
return noLogin ? null : (
|
|
38
|
+
<Button
|
|
39
|
+
variant='primary'
|
|
40
|
+
className={cn('h-8 w-fit !min-w-0', className)}
|
|
41
|
+
onClick={handleLogin ?? (() => { remember(); void login() })}
|
|
42
|
+
>
|
|
43
|
+
Login
|
|
44
|
+
</Button>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Popover>
|
|
50
|
+
<PopoverTrigger asChild>
|
|
51
|
+
<Button
|
|
52
|
+
variant='outline'
|
|
53
|
+
size='icon'
|
|
54
|
+
className={cn('rounded-full text-muted border-2 border-muted bg-level-1 hover:bg-level-2 hover:border-foreground hover:text-foreground uppercase w-8 h-8', className)}
|
|
55
|
+
>{identity?.initials}</Button>
|
|
56
|
+
</PopoverTrigger>
|
|
57
|
+
<PopoverContent className='bg-level-0'>
|
|
58
|
+
<div className='grid gap-4'>
|
|
59
|
+
<div className='space-y-2 truncate'>
|
|
60
|
+
<h4 className='font-medium leading-none truncate'>{identity?.name}</h4>
|
|
61
|
+
{identity?.email && identity.email !== identity.name && (
|
|
62
|
+
<p className='text-sm opacity-50 truncate'>{identity.email}</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<Separator />
|
|
66
|
+
<Button variant='outline' onClick={() => { logout() }}>Logout</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</PopoverContent>
|
|
69
|
+
</Popover>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The widget rides in shared chrome, and not every site mounts <Auth> —
|
|
74
|
+
app/credit carries its own root layout. A site that offers no sign-in shows
|
|
75
|
+
no sign-in, which is why the context is read before any hook that needs it. */
|
|
76
|
+
const AuthWidget: React.FC<Props> = (props) => (
|
|
77
|
+
useContext(IamContext) ? <Widget {...props} /> : null
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export default AuthWidget
|
|
@@ -4,7 +4,7 @@ import React from 'react'
|
|
|
4
4
|
import { Button, Card } from '@hanzo/ui/primitives'
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
6
|
|
|
7
|
-
import { LuxLogo } from '
|
|
7
|
+
import { LuxLogo } from '@luxfi/logo'
|
|
8
8
|
import type { ChatbotSuggestedQuestion } from '../types'
|
|
9
9
|
|
|
10
10
|
const ChatWidget: React.FC<{
|
|
@@ -58,16 +58,14 @@ const ChatWidget: React.FC<{
|
|
|
58
58
|
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
|
59
59
|
<h1 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h1>
|
|
60
60
|
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
|
61
|
-
<LuxLogo
|
|
61
|
+
<LuxLogo variant='white' size={24}/>
|
|
62
62
|
</Button>
|
|
63
63
|
</div>
|
|
64
64
|
<iframe src={iframeSrc} className='h-full' />
|
|
65
65
|
</Card>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
-
<
|
|
69
|
-
width={28}
|
|
70
|
-
height={28}
|
|
68
|
+
<span
|
|
71
69
|
onClick={onClick}
|
|
72
70
|
className={cn(
|
|
73
71
|
// z-index should be below anything in commerce-iu (buy drawer and checkout widget)
|
|
@@ -77,8 +75,9 @@ const ChatWidget: React.FC<{
|
|
|
77
75
|
showChatbot ? 'rotate-180' : '',
|
|
78
76
|
buttonClx
|
|
79
77
|
)}
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
>
|
|
79
|
+
<LuxLogo variant='white' size={28} />
|
|
80
|
+
</span>
|
|
82
81
|
</>)
|
|
83
82
|
}
|
|
84
83
|
|
|
@@ -3,7 +3,7 @@ import React, { type PropsWithChildren } from 'react'
|
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
5
|
import { ScrollArea, StepIndicator } from '@hanzo/ui/primitives'
|
|
6
|
-
import
|
|
6
|
+
import AuthWidget from '../../auth/widget'
|
|
7
7
|
import { CartPanel, useCommerce } from '@hanzo/commerce'
|
|
8
8
|
import { cn } from '@hanzo/ui/util'
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ const DesktopCheckoutPanel: React.FC<PropsWithChildren & CheckoutPanelProps> = o
|
|
|
26
26
|
<div key={1} className='w-full h-full bg-background flex flex-row items-start justify-end'>
|
|
27
27
|
<div className='w-full h-full max-w-[750px] relative flex flex-col items-stretch justify-start px-8 pb-8'>
|
|
28
28
|
<div key={1} className='h-[80px] grow-0 flex flex-row items-center z-10' >
|
|
29
|
-
<Logo size='md' href='/' onClick={onLeave} variant='
|
|
29
|
+
<Logo size='md' href='/' onClick={onLeave} variant='wordmark' outerClx='logo-outer-tooltip-class' />
|
|
30
30
|
<Tooltip select='.logo-outer-tooltip-class' text='home' position='right' offset={6}/>
|
|
31
31
|
</div>
|
|
32
32
|
<BackButton
|
|
@@ -3,7 +3,7 @@ import React, { type PropsWithChildren } from 'react'
|
|
|
3
3
|
|
|
4
4
|
import { StepIndicator } from '@hanzo/ui/primitives'
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
|
-
import
|
|
6
|
+
import AuthWidget from '../../auth/widget'
|
|
7
7
|
|
|
8
8
|
import { BackButton, Logo } from '../..'
|
|
9
9
|
import BagButton from '../bag-button'
|
|
@@ -31,7 +31,7 @@ const MobileCheckoutPanel: React.FC<PropsWithChildren & CheckoutPanelProps> = ({
|
|
|
31
31
|
}
|
|
32
32
|
onBack={onLeave}
|
|
33
33
|
/>
|
|
34
|
-
<Logo size='xs' variant='
|
|
34
|
+
<Logo size='xs' variant='wordmark' href='/' onClick={onLeave} outerClx='-ml-2'/>
|
|
35
35
|
</div>
|
|
36
36
|
<StepIndicator
|
|
37
37
|
dotSizeRem={1}
|
package/components/footer.tsx
CHANGED
|
@@ -34,7 +34,7 @@ const Footer: React.FC<{
|
|
|
34
34
|
'md:flex md:flex-row md:justify-between px-[24px]'
|
|
35
35
|
}>
|
|
36
36
|
<div className='hidden lg:flex flex-col' key={0}>
|
|
37
|
-
<Logo size='md' variant='
|
|
37
|
+
<Logo size='md' variant='wordmark' />
|
|
38
38
|
</div>
|
|
39
39
|
{footer.map((defs: LinkDef[], index: number) => {
|
|
40
40
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { type PropsWithChildren } from 'react'
|
|
2
2
|
|
|
3
3
|
import { cn } from '@hanzo/ui/util'
|
|
4
|
-
import
|
|
4
|
+
import AuthWidget from '../auth/widget'
|
|
5
5
|
|
|
6
6
|
import Logo, { type LogoVariant } from '../logo'
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ const DesktopHeader: React.FC<{
|
|
|
25
25
|
noAuth=false,
|
|
26
26
|
noCommerce=false,
|
|
27
27
|
children,
|
|
28
|
-
logoVariant='
|
|
28
|
+
logoVariant='full'
|
|
29
29
|
}) => {
|
|
30
30
|
const [isMenuOpened, setIsMenuOpen] = React.useState(false);
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ const DesktopHeader: React.FC<{
|
|
|
42
42
|
'flex flex-row h-[80px] items-center justify-between ' +
|
|
43
43
|
'mx-[24px] w-full max-w-screen'
|
|
44
44
|
}>
|
|
45
|
-
<Logo size={logoVariant === '
|
|
45
|
+
<Logo size={logoVariant === 'mark' ? 'lg' : 'md'} href='/' outerClx='hidden lg:flex' key='two' variant={logoVariant} />
|
|
46
46
|
<Logo size='sm' href='/' outerClx='hidden md:flex lg:hidden' key='one' variant={logoVariant} />
|
|
47
47
|
{/* md or larger */}
|
|
48
48
|
<div className='flex w-full gap-4 items-center justify-center'>
|
|
@@ -52,7 +52,7 @@ const DesktopHeader: React.FC<{
|
|
|
52
52
|
{!noCommerce && (
|
|
53
53
|
<DesktopBagPopup popupClx='w-[340px]' trigger={<BagButton className='text-primary -mr-[3px] lg:min-w-0' />} />
|
|
54
54
|
)}
|
|
55
|
-
<AuthWidget
|
|
55
|
+
{!noAuth && <AuthWidget />}
|
|
56
56
|
{children}
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
@@ -15,7 +15,7 @@ const MobileNavMenuAI: React.FC<MobileNavMenuAIProps> = ({ setMenuOpen }) => {
|
|
|
15
15
|
<>
|
|
16
16
|
<div className="w-full text-2xl cursor-pointer">
|
|
17
17
|
<div className='flex justify-between'>
|
|
18
|
-
<Logo variant='
|
|
18
|
+
<Logo variant='wordmark' size='md' outerClx={'p-6 h-full'} />
|
|
19
19
|
<Plus width={28} height={28} className={
|
|
20
20
|
'block h-full aspect-square hover:bg-background sm:hover:bg-level-1 active:scale-75 text-foreground will-change-transform transition-transform transition-scale transition-duration-[1500] mt-6 mr-6 ' +
|
|
21
21
|
(!open ? 'rotate-none' : 'rotate-[135deg] scale-110')
|
|
@@ -5,7 +5,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
5
5
|
import type { LinkDefExtended, ChildMenu } from '../../site-def/main-nav'
|
|
6
6
|
import MobileNavMenuAI from './mobile-nav-menu-ai'
|
|
7
7
|
import MobileNavMenuItem from './mobile-nav-menu-item'
|
|
8
|
-
import
|
|
8
|
+
import AuthWidget from '../auth/widget'
|
|
9
9
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@hanzo/ui/primitives'
|
|
10
10
|
import { ChevronDown } from 'lucide-react'
|
|
11
11
|
import Link from 'next/link'
|
|
@@ -94,7 +94,7 @@ const MobileNav: React.FC<{
|
|
|
94
94
|
})}
|
|
95
95
|
</Accordion>
|
|
96
96
|
</div>
|
|
97
|
-
<
|
|
97
|
+
<AuthWidget className='text-2xl z-10' handleLogin={() => { setMenuState('login') }} />
|
|
98
98
|
</div>
|
|
99
99
|
) : null
|
|
100
100
|
);
|
|
@@ -6,7 +6,7 @@ import type { LinkDef } from '@hanzo/ui/types'
|
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
8
|
import { CartPanel, useCommerce } from '@hanzo/commerce'
|
|
9
|
-
import
|
|
9
|
+
import Login from '../auth/login'
|
|
10
10
|
import sendGAEvent from '../../next/analytics/google-analytics'
|
|
11
11
|
|
|
12
12
|
import { Avatar, Bag } from '../icons'
|
|
@@ -95,7 +95,7 @@ const MobileHeader: React.FC<{
|
|
|
95
95
|
)}>
|
|
96
96
|
{/* smaller than md: mobile style drawer menu; h-11 is 44px, the standard mobile header height */}
|
|
97
97
|
<div className='w-full h-full flex flex-row justify-between items-center font-bold pr-5'>
|
|
98
|
-
<Logo href='/' size='md' outerClx={'p-6 h-full'} variant='
|
|
98
|
+
<Logo href='/' size='md' outerClx={'p-6 h-full'} variant='wordmark' />
|
|
99
99
|
{/* Not that key to the cross-fade effect
|
|
100
100
|
is that this is **on top of** the logo. */}
|
|
101
101
|
{menuOpen() && (
|
|
@@ -129,7 +129,7 @@ const MobileHeader: React.FC<{
|
|
|
129
129
|
'flex flex-column bg-background z-below-header animate-mobile-menu-open'
|
|
130
130
|
}>
|
|
131
131
|
{(!!!noAuth && menuState === 'login') ? (
|
|
132
|
-
<
|
|
132
|
+
<Login onSuccess={onLoginChanged} className='sm:animate-in sm:zoom-in-90 m-auto w-full max-w-sm px-8' />
|
|
133
133
|
) : (
|
|
134
134
|
(!!!noCommerce && menuState === 'bag') ? (
|
|
135
135
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as Avatar } from './avatar'
|
|
2
2
|
export { default as Bag } from './bag-icon'
|
|
3
3
|
export { default as LeftArrow } from './left-arrow'
|
|
4
|
-
export { default as LuxLogo } from './lux-logo'
|
|
5
4
|
export { default as RightArrow } from './right-arrow'
|
|
6
5
|
export { default as SocialIcon, type SocialIconProps } from './social-icon'
|
package/components/index.ts
CHANGED
|
@@ -10,7 +10,8 @@ export { default as Main } from './main'
|
|
|
10
10
|
export { default as MiniChart } from './mini-chart'
|
|
11
11
|
export { default as NotFound } from './not-found'
|
|
12
12
|
|
|
13
|
-
export { default as
|
|
13
|
+
export { default as Auth } from './auth/provider'
|
|
14
|
+
export { default as Login } from './auth/login'
|
|
14
15
|
export { default as BackButton } from './back-button'
|
|
15
16
|
export { default as BuyDrawer } from './commerce/drawer'
|
|
16
17
|
export { default as DrawerMargin } from './drawer-margin'
|
|
@@ -18,7 +19,6 @@ export { default as BuyButton } from './commerce/buy-button'
|
|
|
18
19
|
export { default as CheckoutButton } from './commerce/checkout-button'
|
|
19
20
|
export { default as CheckoutPanel } from './commerce/checkout-panel'
|
|
20
21
|
export { default as LoginPanel } from './auth/login-panel'
|
|
21
|
-
export { default as SignupPanel } from './auth/signup-panel'
|
|
22
22
|
export { default as Analytics } from './analytics'
|
|
23
23
|
export { default as Tooltip } from './tooltip'
|
|
24
24
|
|
package/components/logo.tsx
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import Link from 'next/link'
|
|
3
3
|
|
|
4
|
-
import { type TShirtSize
|
|
4
|
+
import { type TShirtSize } from '@hanzo/ui/types'
|
|
5
|
+
import { Logo as Mark, Wordmark } from '@luxfi/logo'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* mark — the triangle alone
|
|
9
|
+
* wordmark — the LUX letterforms alone
|
|
10
|
+
* full — the mark beside the wordmark
|
|
11
|
+
*
|
|
12
|
+
* Both halves come from @luxfi/logo, which is the one home for Lux brand art.
|
|
13
|
+
* This file used to draw its own triangle and set the letters "LUX" in the body
|
|
14
|
+
* font, so every Lux site wore typed text where the wordmark belongs — and no
|
|
15
|
+
* variant could fix it, because neither the mark nor the wordmark was here to
|
|
16
|
+
* ask for. Art lives in the brand package; this composes it.
|
|
17
|
+
*/
|
|
18
|
+
type LogoVariant = 'mark' | 'wordmark' | 'full'
|
|
10
19
|
|
|
20
|
+
/** Mark height in px, and the wordmark height that stands with it. */
|
|
21
|
+
const SIZE: Record<string, { mark: number, word: number, gap: string }> = {
|
|
22
|
+
xl: { mark: 40, word: 22, gap: 'gap-4' },
|
|
23
|
+
lg: { mark: 40, word: 22, gap: 'gap-4' },
|
|
24
|
+
md: { mark: 32, word: 18, gap: 'gap-3' },
|
|
25
|
+
sm: { mark: 24, word: 13, gap: 'gap-2' },
|
|
26
|
+
xs: { mark: 16, word: 9, gap: 'gap-1.5' },
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
const Logo: React.FC<{
|
|
13
30
|
size?: TShirtSize
|
|
@@ -15,74 +32,29 @@ const Logo: React.FC<{
|
|
|
15
32
|
onClick?: () => void
|
|
16
33
|
href?: string
|
|
17
34
|
outerClx?: string
|
|
18
|
-
textClx?: string
|
|
19
35
|
}> = ({
|
|
20
|
-
size,
|
|
36
|
+
size = 'md',
|
|
21
37
|
href, // no default please!
|
|
22
|
-
outerClx='',
|
|
23
|
-
|
|
24
|
-
variant='full',
|
|
38
|
+
outerClx = '',
|
|
39
|
+
variant = 'full',
|
|
25
40
|
onClick,
|
|
26
41
|
}) => {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
icon: ''
|
|
32
|
-
}
|
|
33
|
-
:
|
|
34
|
-
(variant === 'text-only') ?
|
|
35
|
-
{
|
|
36
|
-
span: '',
|
|
37
|
-
icon: 'hidden'
|
|
38
|
-
}
|
|
39
|
-
:
|
|
40
|
-
{
|
|
41
|
-
span: '',
|
|
42
|
-
icon: ''
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (size === 'lg' || size === 'xl' ) { // for safety
|
|
46
|
-
classes.icon = 'h-10 w-10 mr-4 color-inherit '
|
|
47
|
-
classes.span = 'text-3xl '
|
|
48
|
-
}
|
|
49
|
-
// match lux.network
|
|
50
|
-
else if (size === 'md') {
|
|
51
|
-
classes.icon = 'h-[32px] w-[32px] mr-[12px] color-inherit '
|
|
52
|
-
classes.span = 'text-[1.8rem]/[1.8rem] tracking-tighter '
|
|
53
|
-
}
|
|
54
|
-
else if (size === 'sm' ) {
|
|
55
|
-
classes.icon = 'h-6 w-6 mr-2 color-inherit '
|
|
56
|
-
classes.span = 'text-lg '
|
|
57
|
-
}
|
|
58
|
-
// xs
|
|
59
|
-
else {
|
|
60
|
-
classes.icon = 'h-4 w-4 mr-1 color-inherit '
|
|
61
|
-
classes.span = 'text-base '
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
classes.icon += toAdd.icon
|
|
65
|
-
classes.span += toAdd.span
|
|
66
|
-
|
|
42
|
+
const s = SIZE[size as string] ?? SIZE.md
|
|
43
|
+
const clx = 'flex flex-row items-center ' + s.gap + ' '
|
|
44
|
+
+ (href ? 'hover:opacity-80 cursor-pointer ' : 'cursor-default ')
|
|
45
|
+
+ outerClx
|
|
67
46
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
47
|
+
// White on both counts: the Lux ground is black and the brand's accent is
|
|
48
|
+
// white, so the mark and the letters carry the same value.
|
|
49
|
+
const art = (<>
|
|
50
|
+
{variant !== 'wordmark' && <Mark variant='white' size={s.mark} />}
|
|
51
|
+
{variant !== 'mark' && <Wordmark height={s.word} />}
|
|
52
|
+
</>)
|
|
73
53
|
|
|
74
|
-
return (
|
|
75
|
-
href
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<span className={spanClasses}>{TEXT}</span>
|
|
79
|
-
</Link>
|
|
80
|
-
) : (
|
|
81
|
-
<span className={outerClasses} onClick={onClick}>
|
|
82
|
-
<LuxLogo className={classes.icon} />
|
|
83
|
-
<span className={spanClasses}>{TEXT}</span>
|
|
84
|
-
</span>
|
|
85
|
-
)
|
|
54
|
+
return href ? (
|
|
55
|
+
<Link href={href} className={clx} onClick={onClick}>{art}</Link>
|
|
56
|
+
) : (
|
|
57
|
+
<span className={clx} onClick={onClick}>{art}</span>
|
|
86
58
|
)
|
|
87
59
|
}
|
|
88
60
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { ApplyTypography } from '@hanzo/ui/primitives'
|
|
4
|
+
|
|
5
|
+
import type { SiteDef } from '../../site-def'
|
|
6
|
+
import Footer from '../footer'
|
|
7
|
+
import Header from '../header'
|
|
8
|
+
import Main from '../main'
|
|
9
|
+
|
|
10
|
+
const Security: React.FC<{
|
|
11
|
+
contact: string
|
|
12
|
+
header?: boolean
|
|
13
|
+
siteDef: SiteDef
|
|
14
|
+
}> = ({
|
|
15
|
+
contact,
|
|
16
|
+
header = false,
|
|
17
|
+
siteDef
|
|
18
|
+
}) => (<>
|
|
19
|
+
{header && <Header siteDef={siteDef} />}
|
|
20
|
+
<Main className='px-8 sm:px-10 pb-16'>
|
|
21
|
+
<ApplyTypography className='w-full max-w-[46rem] mx-auto flex flex-col gap-6 pt-12'>
|
|
22
|
+
<h1>Reporting a security problem</h1>
|
|
23
|
+
<p>
|
|
24
|
+
Email <a href={`mailto:${contact}`}>{contact}</a>. Tell us what you found and how to
|
|
25
|
+
reproduce it. Send it to us before you tell anyone else, and do not open a public issue.
|
|
26
|
+
</p>
|
|
27
|
+
<p>
|
|
28
|
+
A real report from a stranger is worth more than another internal review. We read every one.
|
|
29
|
+
</p>
|
|
30
|
+
<h2>What happens next</h2>
|
|
31
|
+
<p>
|
|
32
|
+
Someone will read your report and reply. If we can reproduce the problem we will tell you
|
|
33
|
+
what we are doing about it, and tell you again once it is fixed. If we cannot reproduce it
|
|
34
|
+
we will say so and ask you for what we are missing.
|
|
35
|
+
</p>
|
|
36
|
+
<h2>While you are looking</h2>
|
|
37
|
+
<p>
|
|
38
|
+
Test against accounts and data that are your own. Do not read, change, or keep data that
|
|
39
|
+
belongs to someone else. If you reach it by accident, stop, and say so in your report.
|
|
40
|
+
</p>
|
|
41
|
+
<p>
|
|
42
|
+
Do not run anything that degrades the service for other people. No load testing, no denial
|
|
43
|
+
of service, no mass automated scanning, and no social engineering of our staff or our users.
|
|
44
|
+
</p>
|
|
45
|
+
<h2>For scanners</h2>
|
|
46
|
+
<p>
|
|
47
|
+
This page is the policy named by{' '}
|
|
48
|
+
<a href='/.well-known/security.txt'>/.well-known/security.txt</a>, per RFC 9116.
|
|
49
|
+
</p>
|
|
50
|
+
</ApplyTypography>
|
|
51
|
+
</Main>
|
|
52
|
+
<Footer siteDef={siteDef} />
|
|
53
|
+
</>)
|
|
54
|
+
|
|
55
|
+
export default Security
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse, userAgent } from 'next/server'
|
|
2
2
|
import { getSelectorsByUserAgent } from 'react-device-detect'
|
|
3
|
-
import { setCookie } from 'cookies-next'
|
|
4
3
|
|
|
5
4
|
// writed this way so they can be chained :)
|
|
6
5
|
const determineDeviceMW = async (request: NextRequest) => {
|
|
@@ -10,18 +9,6 @@ const determineDeviceMW = async (request: NextRequest) => {
|
|
|
10
9
|
const agent = isMobileOnly ? 'phone' : (isTablet ? 'tablet' : (isDesktop ? 'desktop' : 'unknown'))
|
|
11
10
|
const { nextUrl: url } = request
|
|
12
11
|
//console.log(`\n=== from ${url.href} on a *${agent && agent.toUpperCase()}* device. ===\n`)
|
|
13
|
-
const auth_token = url.searchParams.get('auth-token')
|
|
14
|
-
if (auth_token) {
|
|
15
|
-
setCookie('auth-token', auth_token, {
|
|
16
|
-
domain: url.hostname,
|
|
17
|
-
path: '/',
|
|
18
|
-
sameSite: 'none',
|
|
19
|
-
secure: true,
|
|
20
|
-
httpOnly: false,
|
|
21
|
-
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
|
|
22
|
-
})
|
|
23
|
-
url.searchParams.delete('auth-token')
|
|
24
|
-
}
|
|
25
12
|
url.searchParams.set('agent', agent)
|
|
26
13
|
return NextResponse.rewrite(url)
|
|
27
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxfi/ui",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.5",
|
|
4
4
|
"description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./components/index.ts",
|
|
23
|
+
"./auth-callback": "./components/auth/callback.tsx",
|
|
24
|
+
"./security": "./components/security/index.tsx",
|
|
23
25
|
"./commerce": "./commerce/ui/context.tsx",
|
|
24
26
|
"./root-layout": "./root-layout/index.tsx",
|
|
25
27
|
"./next": "./next/index.ts",
|
|
@@ -29,11 +31,10 @@
|
|
|
29
31
|
"./util": "./util/index.ts"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
34
|
+
"@luxfi/logo": "^1.0.10",
|
|
32
35
|
"@next/third-parties": "^16.1.0",
|
|
33
|
-
"cookies-next": "^4.1.1",
|
|
34
36
|
"date-fns": "^3.6.0",
|
|
35
37
|
"embla-carousel-autoplay": "^8.1.6",
|
|
36
|
-
"firebase": "10.12.0",
|
|
37
38
|
"framer-motion": "^11.2.12",
|
|
38
39
|
"markdown-to-jsx": "^7.4.7",
|
|
39
40
|
"react-device-detect": "^2.2.3",
|
|
@@ -43,9 +44,10 @@
|
|
|
43
44
|
"usehooks-ts": "^3.1.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
|
-
"@hanzo/
|
|
47
|
-
"@hanzo/commerce": "7.
|
|
47
|
+
"@hanzo/iam": "0.21.9",
|
|
48
|
+
"@hanzo/commerce": "7.6.4",
|
|
48
49
|
"@hanzo/ui": "5.3.34",
|
|
50
|
+
"@luxfi/menu-icons": "^1.0.2",
|
|
49
51
|
"@hookform/resolvers": "^3.3.2",
|
|
50
52
|
"lucide-react": "0.470.0",
|
|
51
53
|
"mobx": "^6.12.3",
|
|
@@ -56,8 +58,7 @@
|
|
|
56
58
|
"react-dom": "19.2.0",
|
|
57
59
|
"react-hook-form": "7.54.2",
|
|
58
60
|
"validator": "^13.11.0",
|
|
59
|
-
"zod": "3.23.8"
|
|
60
|
-
"@luxfi/menu-icons": "1.0.2"
|
|
61
|
+
"zod": "3.23.8"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@hookform/resolvers": "^3.3.2",
|
package/root-layout/index.tsx
CHANGED
|
@@ -2,15 +2,13 @@ import React, { type PropsWithChildren } from 'react'
|
|
|
2
2
|
import type { Viewport } from 'next'
|
|
3
3
|
|
|
4
4
|
import { Toaster } from '@hanzo/ui/primitives'
|
|
5
|
-
import { AuthServiceProvider } from '@hanzo/auth/service'
|
|
6
|
-
import type { AuthServiceConf } from '@hanzo/auth/types'
|
|
7
5
|
import { CommerceProvider } from '@hanzo/commerce'
|
|
8
6
|
|
|
9
7
|
import getAppRouterBodyFontClasses from '../next/font/get-app-router-font-classes'
|
|
10
8
|
import { FacebookPixelHead } from '../next/analytics/pixel-analytics'
|
|
11
9
|
|
|
12
10
|
import { CommerceUIProvider } from '../commerce/ui/context'
|
|
13
|
-
import {
|
|
11
|
+
import { Auth, ChatWidget, Header, Analytics } from '../components'
|
|
14
12
|
|
|
15
13
|
import CommerceDrawer from '../components/commerce/drawer'
|
|
16
14
|
|
|
@@ -57,10 +55,6 @@ function RootLayout({
|
|
|
57
55
|
chatbot?: boolean
|
|
58
56
|
} & PropsWithChildren) {
|
|
59
57
|
|
|
60
|
-
// For static export, we don't have server-side auth
|
|
61
|
-
// User auth will be handled client-side via AuthListener
|
|
62
|
-
const currentUser = null
|
|
63
|
-
|
|
64
58
|
const Guts: React.FC = () => (<>
|
|
65
59
|
{showHeader && <Header siteDef={siteDef}/>}
|
|
66
60
|
{children}
|
|
@@ -87,7 +81,7 @@ function RootLayout({
|
|
|
87
81
|
display: 'none', // see analytics.tsx
|
|
88
82
|
}}>
|
|
89
83
|
<Analytics/>
|
|
90
|
-
<
|
|
84
|
+
<Auth app={siteDef.app}>
|
|
91
85
|
{siteDef?.commerce ? (
|
|
92
86
|
<CommerceProvider config={siteDef.commerce!} >
|
|
93
87
|
<CommerceUIProvider >
|
|
@@ -98,8 +92,7 @@ function RootLayout({
|
|
|
98
92
|
) : (
|
|
99
93
|
<Guts />
|
|
100
94
|
)}
|
|
101
|
-
|
|
102
|
-
</AuthServiceProvider>
|
|
95
|
+
</Auth>
|
|
103
96
|
<Toaster position='top-center' duration={3000}/>
|
|
104
97
|
</body>
|
|
105
98
|
</html>
|
package/style/lux-global.css
CHANGED
|
@@ -18,6 +18,55 @@ div.nextjs-toast-errors-parent[data-nextjs-toast="true"] {
|
|
|
18
18
|
@apply uppercase;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/* Hanzo IAM's sign-in views ship structure, not skin. This is the one place
|
|
22
|
+
* that markup meets the Lux surface -- colours come from lux-colors so the
|
|
23
|
+
* form follows the theme like everything else.
|
|
24
|
+
*/
|
|
25
|
+
.hanzo-iam-card,
|
|
26
|
+
.hanzo-iam-form {
|
|
27
|
+
@apply flex flex-col gap-3 w-full;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.hanzo-iam-form input {
|
|
31
|
+
@apply w-full rounded-sm px-3 py-2 text-base;
|
|
32
|
+
background-color: var(--hz-ui-bg-1);
|
|
33
|
+
color: var(--hz-ui-fg-body);
|
|
34
|
+
border: 1px solid var(--hz-ui-bg-2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hanzo-iam-form input::placeholder {
|
|
38
|
+
color: var(--hz-ui-fg-3);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.hanzo-iam-form input:focus {
|
|
42
|
+
outline: none;
|
|
43
|
+
border-color: var(--hz-ui-fg-body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.hanzo-iam-btn {
|
|
47
|
+
@apply w-full rounded-sm px-3 py-2 text-base font-semibold cursor-pointer;
|
|
48
|
+
background-color: var(--hz-ui-bg-inverted);
|
|
49
|
+
color: var(--hz-ui-fg-inverted);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.hanzo-iam-btn:hover {
|
|
53
|
+
background-color: var(--hz-ui-bg-inverted-hover);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hanzo-iam-btn:disabled {
|
|
57
|
+
@apply opacity-50 cursor-default;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.hanzo-iam-divider {
|
|
61
|
+
@apply text-sm uppercase;
|
|
62
|
+
color: var(--hz-ui-fg-3);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.hanzo-iam-error {
|
|
66
|
+
@apply text-sm;
|
|
67
|
+
color: hsl(0 70% 60%);
|
|
68
|
+
}
|
|
69
|
+
|
|
21
70
|
/* Specific style fixes for react-square-web-payments-sdk -
|
|
22
71
|
* reduce gap between card input and pay button
|
|
23
72
|
*/
|
package/types/site-def.ts
CHANGED
|
@@ -7,6 +7,13 @@ import type ChatbotConfig from './chatbot-config'
|
|
|
7
7
|
|
|
8
8
|
interface SiteDef {
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Short name of this site, which with the org names the IAM application it
|
|
12
|
+
* signs in as: 'quest' is the client `lux-quest` on lux.id. Required, so a
|
|
13
|
+
* site cannot ship a login button that belongs to no one.
|
|
14
|
+
*/
|
|
15
|
+
app: string
|
|
16
|
+
|
|
10
17
|
/** url of this site. All nav links in the system will show it in 'current' state */
|
|
11
18
|
currentAs?: string
|
|
12
19
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
|
-
import { useAuth } from '@hanzo/auth/service'
|
|
5
|
-
import { getCookie } from 'cookies-next'
|
|
6
|
-
|
|
7
|
-
const AuthListener = () => {
|
|
8
|
-
const auth = useAuth()
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
// Sites that do not share a login origin leave this unset; without it there
|
|
12
|
-
// is nothing to ask, so asking would only fetch the string "undefined".
|
|
13
|
-
const loginSite = process.env.NEXT_PUBLIC_LOGIN_SITE_URL
|
|
14
|
-
if (!loginSite) return
|
|
15
|
-
|
|
16
|
-
fetch(`${loginSite}/api/auth/get-auth-token`, {
|
|
17
|
-
method: 'GET',
|
|
18
|
-
credentials: 'include',
|
|
19
|
-
})
|
|
20
|
-
.then(response => response.json())
|
|
21
|
-
.then((data: any) => {
|
|
22
|
-
const token = data.reqToken
|
|
23
|
-
if (!!token) {
|
|
24
|
-
auth.loginWithCustomToken(token)
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
.catch(() => { /* no shared session available */ })
|
|
28
|
-
}, [auth])
|
|
29
|
-
|
|
30
|
-
return ( <></> )
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default AuthListener
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import domains from '../common-auth-domains'
|
|
2
|
-
|
|
3
|
-
const ClearAuthToken = () => {
|
|
4
|
-
return (<>
|
|
5
|
-
{domains.map(({url}) => (
|
|
6
|
-
/* Clear auth-token cookie across all Lux domains */
|
|
7
|
-
<img src={`${url}/api/auth/clear-auth-token`} className='absolute hidden'/>
|
|
8
|
-
))}
|
|
9
|
-
</>)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default ClearAuthToken
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import domains from '../common-auth-domains'
|
|
2
|
-
|
|
3
|
-
const SetAuthToken: React.FC<{
|
|
4
|
-
authToken: string
|
|
5
|
-
}> = ({
|
|
6
|
-
authToken,
|
|
7
|
-
}) => {
|
|
8
|
-
return (<>
|
|
9
|
-
{!!authToken && domains.map(({url}) => (
|
|
10
|
-
/* Set auth-token cookie across all Lux domains */
|
|
11
|
-
<img src={`${url}/api/auth/set-auth-token?token=${authToken}`} className='absolute hidden'/>
|
|
12
|
-
))}
|
|
13
|
-
</>)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default SetAuthToken
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const domains = [
|
|
2
|
-
{ id: 'lux.market', url: 'https://lux.market' },
|
|
3
|
-
{ id: 'lux.shop', url: 'https://lux.shop' },
|
|
4
|
-
{ id: 'lux.credit', url: 'https://lux.credit' },
|
|
5
|
-
{ id: 'lux.network', url: 'https://lux.network' },
|
|
6
|
-
{ id: 'wallet.lux.network', url: 'https://wallet.lux.network' },
|
|
7
|
-
{ id: 'safe.lux.network', url: 'https://safe.lux.network' },
|
|
8
|
-
{ id: 'lux.finance', url: 'https://lux.finance' },
|
|
9
|
-
{ id: 'lux.exchange', url: 'https://lux.exchange' },
|
|
10
|
-
{ id: 'lux.quest', url: 'https://lux.quest' },
|
|
11
|
-
{ id: 'lux.id', url: 'https://lux.id' },
|
|
12
|
-
{ id: 'lux.chat', url: 'https://lux.chat' }
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
domains as default
|
|
17
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React from "react"
|
|
3
|
-
import { observer } from "mobx-react-lite"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
Button,
|
|
7
|
-
LinkElement,
|
|
8
|
-
Popover,
|
|
9
|
-
PopoverContent,
|
|
10
|
-
PopoverTrigger,
|
|
11
|
-
Separator
|
|
12
|
-
} from '@hanzo/ui/primitives'
|
|
13
|
-
|
|
14
|
-
import type { LinkDef } from '@hanzo/ui/types'
|
|
15
|
-
import { cn } from '@hanzo/ui/util'
|
|
16
|
-
|
|
17
|
-
import { useAuth } from "@hanzo/auth/service"
|
|
18
|
-
|
|
19
|
-
import { Ethereum } from "@hanzo/auth/icons"
|
|
20
|
-
|
|
21
|
-
const MobileAuthWidget: React.FC<{
|
|
22
|
-
noLogin?: boolean
|
|
23
|
-
className?: string
|
|
24
|
-
handleLogin?: () => void
|
|
25
|
-
}> = observer(({
|
|
26
|
-
noLogin = false,
|
|
27
|
-
className
|
|
28
|
-
}) => {
|
|
29
|
-
const auth = useAuth()
|
|
30
|
-
const handleLogin = () => {
|
|
31
|
-
window.location.href = "https://lux.id";
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (!auth) {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
if (!auth.loggedIn && typeof window !== 'undefined') {
|
|
38
|
-
return (noLogin ? null : (
|
|
39
|
-
(handleLogin) ? (
|
|
40
|
-
<div className="flex items-center py-1 px-1 gap-1">
|
|
41
|
-
<Button
|
|
42
|
-
variant='primary'
|
|
43
|
-
className='text-base font-semibold !min-w-0 self-center flex-1'
|
|
44
|
-
onClick={handleLogin}
|
|
45
|
-
>
|
|
46
|
-
Sign Up
|
|
47
|
-
</Button>
|
|
48
|
-
<Button
|
|
49
|
-
variant='outline'
|
|
50
|
-
className=' text-base font-semibold !min-w-0 self-center flex-1'
|
|
51
|
-
onClick={handleLogin}
|
|
52
|
-
>
|
|
53
|
-
Log In
|
|
54
|
-
</Button>
|
|
55
|
-
</div>
|
|
56
|
-
) : (
|
|
57
|
-
// Without a login origin there is nowhere to send them, and the
|
|
58
|
-
// link would read `undefined?redirectUrl=…` — which next/link
|
|
59
|
-
// prefetches, so every page fetches a 404 it can never use.
|
|
60
|
-
process.env.NEXT_PUBLIC_LOGIN_SITE_URL ? (
|
|
61
|
-
<LinkElement
|
|
62
|
-
def={{
|
|
63
|
-
href: `${process.env.NEXT_PUBLIC_LOGIN_SITE_URL}?redirectUrl=${window.location.href}`,
|
|
64
|
-
title: 'Login',
|
|
65
|
-
variant: 'primary',
|
|
66
|
-
newTab: false
|
|
67
|
-
} satisfies LinkDef}
|
|
68
|
-
className='h-8 w-fit !min-w-0'
|
|
69
|
-
/>
|
|
70
|
-
) : null
|
|
71
|
-
)
|
|
72
|
-
))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<Popover>
|
|
77
|
-
<PopoverTrigger asChild>
|
|
78
|
-
<Button
|
|
79
|
-
variant="outline"
|
|
80
|
-
size='icon'
|
|
81
|
-
className={cn('rounded-full text-muted border-2 border-muted bg-level-1 hover:bg-level-2 hover:border-foreground hover:text-foreground uppercase w-8 h-8', className)}
|
|
82
|
-
>{auth.user?.email[0]}</Button>
|
|
83
|
-
</PopoverTrigger>
|
|
84
|
-
<PopoverContent className='bg-level-0'>
|
|
85
|
-
<div className="grid gap-4">
|
|
86
|
-
<div className="space-y-2 truncate">
|
|
87
|
-
{auth.user?.displayName ? (
|
|
88
|
-
<>
|
|
89
|
-
<h4 className="font-medium leading-none truncate">{auth.user.displayName}</h4>
|
|
90
|
-
<p className="text-sm opacity-50 truncate">{auth.user.email}</p>
|
|
91
|
-
</>
|
|
92
|
-
) : (
|
|
93
|
-
<h4 className="font-medium leading-none truncate">{auth.user?.email}</h4>
|
|
94
|
-
)}
|
|
95
|
-
{auth.user?.walletAddress ? (
|
|
96
|
-
<p className="text-sm opacity-50 truncate">{auth.user.walletAddress}</p>
|
|
97
|
-
) : (
|
|
98
|
-
<Button variant="outline" className='w-full flex items-center gap-2' onClick={auth.associateWallet.bind(auth)}>
|
|
99
|
-
<Ethereum height={20} />Connect your wallet
|
|
100
|
-
</Button>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
<Separator />
|
|
104
|
-
<Button variant="outline" onClick={auth.logout.bind(auth)}>Logout</Button>
|
|
105
|
-
</div>
|
|
106
|
-
</PopoverContent>
|
|
107
|
-
</Popover>
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
export default MobileAuthWidget
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import Link from 'next/link'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
5
|
-
|
|
6
|
-
import { setCookie } from 'cookies-next'
|
|
7
|
-
|
|
8
|
-
import { cn } from '@hanzo/ui/util'
|
|
9
|
-
import { Button, Carousel, CarouselContent, CarouselItem } from '@hanzo/ui/primitives'
|
|
10
|
-
import { LoginPanel as Login, SignupPanel as Signup } from '@hanzo/auth/components'
|
|
11
|
-
|
|
12
|
-
import { LuxLogo } from '../icons'
|
|
13
|
-
import Logo from '../logo'
|
|
14
|
-
import { EmblaAutoplay } from '..'
|
|
15
|
-
import { legal } from '../../site-def/footer'
|
|
16
|
-
|
|
17
|
-
const SignupPanel: React.FC<{
|
|
18
|
-
close: () => void
|
|
19
|
-
getStartedUrl?: string
|
|
20
|
-
redirectUrl?: string
|
|
21
|
-
className?: string
|
|
22
|
-
reviews: { text: string, author: string, href: string }[]
|
|
23
|
-
setIsLogin?: React.Dispatch<React.SetStateAction<boolean>>
|
|
24
|
-
}> = ({
|
|
25
|
-
close,
|
|
26
|
-
getStartedUrl = '/',
|
|
27
|
-
redirectUrl,
|
|
28
|
-
className = '',
|
|
29
|
-
reviews,
|
|
30
|
-
setIsLogin
|
|
31
|
-
}) => {
|
|
32
|
-
const router = useRouter()
|
|
33
|
-
|
|
34
|
-
const termsOfServiceUrl = legal.find(({ title }) => title === 'Terms and Conditions')?.href || ''
|
|
35
|
-
const privacyPolicyUrl = legal.find(({ title }) => title === 'Privacy Policy')?.href || ''
|
|
36
|
-
const domains = ['lux.id', 'lux.credit', 'lux.market', 'lux.network', 'lux.shop', 'wallet.lux.network', 'safe.lux.network', 'lux.finance', 'lux.exchange', 'lux.quest']
|
|
37
|
-
|
|
38
|
-
// TODO :aa shouldn't this happen in @hanzo/auth: components/LoginPanel ??
|
|
39
|
-
// Otherwise, the functionality is split across modules! (and client/fw!)
|
|
40
|
-
// (This was never my intent w the onLoginChanged callback.)
|
|
41
|
-
const onLogin = (token: string) => {
|
|
42
|
-
for (let i = 0; i < domains.length; i++) {
|
|
43
|
-
setCookie('auth-token', token, {
|
|
44
|
-
domain: domains[i],
|
|
45
|
-
path: '/',
|
|
46
|
-
sameSite: 'none',
|
|
47
|
-
secure: true,
|
|
48
|
-
httpOnly: false,
|
|
49
|
-
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
redirectUrl && router.push(redirectUrl)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className={cn('grid grid-cols-1 md:grid-cols-2', className)}>
|
|
58
|
-
<div className='hidden md:flex w-full h-full bg-[radial-gradient(circle_at_24.1%_68.8%_,_rgba(15,14,14,0)_,_rgba(9,9,9,99.4))] flex-row items-end justify-end overflow-y-auto min-h-screen'>
|
|
59
|
-
<div className='h-full w-full max-w-[750px] px-8 pt-0'>
|
|
60
|
-
<div className='h-full w-full max-w-[550px] mx-auto flex flex-col justify-between min-h-screen py-10'>
|
|
61
|
-
<Button
|
|
62
|
-
variant='ghost'
|
|
63
|
-
onClick={close}
|
|
64
|
-
className='w-fit !min-w-0 p-2'
|
|
65
|
-
>
|
|
66
|
-
<Logo size='md' textClx='!cursor-pointer' variant='text-only' />
|
|
67
|
-
</Button>
|
|
68
|
-
<Carousel
|
|
69
|
-
options={{ align: 'center', loop: true }}
|
|
70
|
-
className='w-full'
|
|
71
|
-
plugins={[EmblaAutoplay({ delay: 5000, stopOnInteraction: true })]}
|
|
72
|
-
>
|
|
73
|
-
<CarouselContent>
|
|
74
|
-
{reviews.map(({ text, author, href }, index) => (
|
|
75
|
-
<CarouselItem key={index}>
|
|
76
|
-
<Link href={href} className='flex flex-col gap-3 cursor-pointer'>
|
|
77
|
-
<p>“{text}“</p>
|
|
78
|
-
<p className='text-sm'>{author}</p>
|
|
79
|
-
</Link>
|
|
80
|
-
</CarouselItem>
|
|
81
|
-
))}
|
|
82
|
-
</CarouselContent>
|
|
83
|
-
</Carousel>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
<div className='w-full h-full bg-background flex flex-row items-center'>
|
|
88
|
-
<div className='w-full max-w-[750px] relative flex flex-col items-center px-8 pt-0 text-center'>
|
|
89
|
-
<div className='relative h-full w-full max-w-[400px] mx-auto flex flex-col gap-4 items-center py-10'>
|
|
90
|
-
<Button
|
|
91
|
-
variant='ghost'
|
|
92
|
-
onClick={close}
|
|
93
|
-
className='block md:hidden absolute rounded-full p-2 left-0 h-auto hover:bg-background'
|
|
94
|
-
>
|
|
95
|
-
<LuxLogo className='w-5 h-5' />
|
|
96
|
-
</Button>
|
|
97
|
-
<Signup
|
|
98
|
-
getStartedUrl={getStartedUrl}
|
|
99
|
-
redirectUrl={redirectUrl}
|
|
100
|
-
className='w-full max-w-sm'
|
|
101
|
-
termsOfServiceUrl={termsOfServiceUrl}
|
|
102
|
-
privacyPolicyUrl={privacyPolicyUrl}
|
|
103
|
-
onLoginChanged={onLogin}
|
|
104
|
-
setIsLogin={setIsLogin}
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default SignupPanel
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { type LucideProps } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
const LuxLogo: React.FC<LucideProps> = (props: LucideProps) => (
|
|
5
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" {...props}>
|
|
6
|
-
<polygon points="25,46.65 50,3.35 0,3.35" fill="white" stroke='black' strokeWidth={props.strokeWidth}/>
|
|
7
|
-
</svg>
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
export default LuxLogo
|