@fayz-ai/plugin-auth 0.9.0 → 0.9.1

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 (44) hide show
  1. package/dist/{AmbassadorBubbles.d.ts → components/AmbassadorBubbles.d.ts} +1 -1
  2. package/dist/components/AmbassadorBubbles.d.ts.map +1 -0
  3. package/dist/components/forms.d.ts.map +1 -0
  4. package/dist/index.d.ts +9 -9
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/{runtime.d.ts → lib/runtime.d.ts} +1 -1
  9. package/dist/lib/runtime.d.ts.map +1 -0
  10. package/dist/{pages.d.ts → views/pages.d.ts} +1 -1
  11. package/dist/views/pages.d.ts.map +1 -0
  12. package/dist/views/set-password.d.ts.map +1 -0
  13. package/dist/website/components/AuthModalProvider.d.ts.map +1 -0
  14. package/dist/website/components/PhoneAuthModal.d.ts.map +1 -0
  15. package/dist/website/index.d.ts +5 -5
  16. package/dist/website/index.d.ts.map +1 -1
  17. package/dist/website/index.js +1 -1
  18. package/dist/website/index.js.map +1 -1
  19. package/dist/website/lib/phone.d.ts.map +1 -0
  20. package/package.json +5 -8
  21. package/dist/AmbassadorBubbles.d.ts.map +0 -1
  22. package/dist/forms.d.ts.map +0 -1
  23. package/dist/pages.d.ts.map +0 -1
  24. package/dist/runtime.d.ts.map +0 -1
  25. package/dist/set-password.d.ts.map +0 -1
  26. package/dist/website/AuthModalProvider.d.ts.map +0 -1
  27. package/dist/website/PhoneAuthModal.d.ts.map +0 -1
  28. package/dist/website/phone.d.ts.map +0 -1
  29. package/src/AmbassadorBubbles.tsx +0 -61
  30. package/src/forms.tsx +0 -307
  31. package/src/index.ts +0 -56
  32. package/src/pages.tsx +0 -209
  33. package/src/runtime.ts +0 -67
  34. package/src/set-password.tsx +0 -229
  35. package/src/types.ts +0 -65
  36. package/src/website/AuthModalProvider.tsx +0 -48
  37. package/src/website/PhoneAuthModal.tsx +0 -178
  38. package/src/website/index.tsx +0 -17
  39. package/src/website/phone.ts +0 -12
  40. /package/dist/{forms.d.ts → components/forms.d.ts} +0 -0
  41. /package/dist/{set-password.d.ts → views/set-password.d.ts} +0 -0
  42. /package/dist/website/{AuthModalProvider.d.ts → components/AuthModalProvider.d.ts} +0 -0
  43. /package/dist/website/{PhoneAuthModal.d.ts → components/PhoneAuthModal.d.ts} +0 -0
  44. /package/dist/website/{phone.d.ts → lib/phone.d.ts} +0 -0
package/src/runtime.ts DELETED
@@ -1,67 +0,0 @@
1
- import type { AuthAdapter } from '@fayz-ai/core'
2
- import { createMockAuthAdapter, createSupabaseAuthAdapter } from '@fayz-ai/auth'
3
- import type { AuthPluginOptions, ResolvedAuthPlugin } from './types'
4
-
5
- function absoluteUrl(path: string): string | undefined {
6
- if (typeof window === 'undefined') return undefined
7
- if (/^https?:\/\//.test(path)) return path
8
- return `${window.location.origin}${path.startsWith('/') ? path : `/${path}`}`
9
- }
10
-
11
- export function resolveAuthAdapter(options: AuthPluginOptions = {}): AuthAdapter {
12
- const configured = options.adapter ?? options.provider
13
- if (configured && typeof configured === 'object') return configured
14
-
15
- const provider = configured ?? (options.supabase?.url ? 'supabase' : 'mock')
16
- if (provider === 'supabase') {
17
- const url = options.supabase?.url
18
- const anonKey = options.supabase?.anonKey
19
- if (!url || !anonKey) {
20
- throw new Error('plugin-auth provider="supabase" requires supabase.url and supabase.anonKey.')
21
- }
22
- return createSupabaseAuthAdapter({
23
- supabaseUrl: url,
24
- supabaseAnonKey: anonKey,
25
- supabaseClient: options.supabase?.client,
26
- callbackUrl: options.supabase?.callbackUrl ?? absoluteUrl(options.routes?.callback ?? '/auth/callback'),
27
- // Only when the host asked for a specific URL. Left undefined, the adapter
28
- // points recovery at the app root, which needs no route and no extra entry
29
- // in Supabase's redirect allow-list.
30
- resetPasswordUrl:
31
- options.supabase?.resetPasswordUrl ??
32
- (options.routes?.resetPassword ? absoluteUrl(options.routes.resetPassword) : undefined),
33
- })
34
- }
35
-
36
- return createMockAuthAdapter()
37
- }
38
-
39
- export function createAuthRuntime(options: AuthPluginOptions = {}): ResolvedAuthPlugin {
40
- return {
41
- kind: 'auth',
42
- adapter: resolveAuthAdapter(options),
43
- requireAuth: options.requireAuth ?? true,
44
- layout: options.layout ?? 'split',
45
- logo: options.logo,
46
- tagline: options.tagline,
47
- description: options.description,
48
- loginAmbassadors: options.loginAmbassadors,
49
- loginAmbassadorsLabel: options.loginAmbassadorsLabel,
50
- oauth: {
51
- enabled: options.oauth?.enabled ?? false,
52
- providers: options.oauth?.providers ?? ['google'],
53
- },
54
- routes: {
55
- login: options.routes?.login ?? '/login',
56
- callback: options.routes?.callback ?? '/auth/callback',
57
- // The app root — recovery links are recognised by their own params, not by
58
- // the path they land on. See resetPassword() in the Supabase adapter.
59
- resetPassword: options.routes?.resetPassword ?? '/',
60
- afterSignIn: options.routes?.afterSignIn ?? '/',
61
- },
62
- }
63
- }
64
-
65
- export function createAuthPlugin(options: AuthPluginOptions = {}): ResolvedAuthPlugin {
66
- return createAuthRuntime(options)
67
- }
@@ -1,229 +0,0 @@
1
- import * as React from 'react'
2
- import { useAuth, readAuthUrlIntent, clearAuthUrlTokens, decidePasswordSetup } from '@fayz-ai/auth'
3
- import { useTranslation } from '@fayz-ai/core'
4
- import { ForgotPasswordForm } from './forms'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Set-password surface — the one screen behind two flows:
8
- //
9
- // • recovery — the user clicked "forgot password" and came back on a recovery
10
- // link. GoTrue already signed them in from the link, so all that is missing
11
- // is `updateUser({ password })`. Without this screen they land straight in
12
- // the app (or on the login form) and are never asked, which is exactly the
13
- // "the link does nothing" bug.
14
- //
15
- // • invite — the user was added to an organisation. The invite is a magic link,
16
- // so the account exists with NO password: unless they choose one here, every
17
- // future sign-in needs another e-mail.
18
- //
19
- // Mounted by the shell above its auth gate, driven by usePasswordSetupGate().
20
- // ---------------------------------------------------------------------------
21
-
22
- export type SetPasswordReason = 'recovery' | 'invite'
23
-
24
- const MIN_LENGTH = 8
25
-
26
- function fieldClass() {
27
- return 'h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50'
28
- }
29
-
30
- function buttonClass() {
31
- return 'inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50'
32
- }
33
-
34
- function useTr() {
35
- const t = useTranslation()
36
- return React.useCallback(
37
- (key: string, fallback: string, params?: Record<string, string | number>) => {
38
- const value = t(key, params)
39
- return !value || value === key ? fallback : value
40
- },
41
- [t],
42
- )
43
- }
44
-
45
- export interface PasswordSetupGate {
46
- /** null when nothing is pending and the shell should render normally. */
47
- reason: SetPasswordReason | null
48
- /** The link is dead (already used / expired) — offer a fresh one instead. */
49
- expired: boolean
50
- /** Still resolving the session the link carries. */
51
- checking: boolean
52
- /** Called by the screen once the password is set (or the user gives up). */
53
- complete: () => void
54
- }
55
-
56
- /**
57
- * Decides whether the user owes us a password before they see the app.
58
- *
59
- * The URL intent is read from a snapshot taken at module load: supabase-js wipes
60
- * the token fragment as soon as it initialises, so the reason the user is here is
61
- * gone by the time React renders.
62
- */
63
- export function usePasswordSetupGate(): PasswordSetupGate {
64
- const { user, isLoading, isAuthenticated } = useAuth()
65
- const [done, setDone] = React.useState(false)
66
- const intent = React.useMemo(() => readAuthUrlIntent(), [])
67
-
68
- const complete = React.useCallback(() => {
69
- clearAuthUrlTokens()
70
- setDone(true)
71
- }, [])
72
-
73
- if (done) return { reason: null, expired: false, checking: false, complete }
74
-
75
- const decision = decidePasswordSetup({
76
- intent,
77
- isLoading,
78
- isAuthenticated,
79
- mustSetPassword: user?.mustSetPassword,
80
- })
81
- return { ...decision, complete }
82
- }
83
-
84
- export interface SetPasswordPageProps {
85
- reason: SetPasswordReason
86
- appName?: string
87
- logo?: React.ReactNode
88
- /** Show the "ask for a new link" form instead of the password fields. */
89
- expired?: boolean
90
- /** Waiting on the link's session — renders a quiet placeholder. */
91
- checking?: boolean
92
- onSuccess?: () => void
93
- }
94
-
95
- export function SetPasswordPage({
96
- reason,
97
- appName = 'App',
98
- logo,
99
- expired,
100
- checking,
101
- onSuccess,
102
- }: SetPasswordPageProps) {
103
- const tr = useTr()
104
- const { user, updatePassword, signOut, error, isLoading } = useAuth()
105
- const [password, setPassword] = React.useState('')
106
- const [confirmPassword, setConfirmPassword] = React.useState('')
107
- const [localError, setLocalError] = React.useState<string | null>(null)
108
- const [busy, setBusy] = React.useState(false)
109
- const disabled = busy || isLoading
110
-
111
- const invite = reason === 'invite'
112
- const title = invite
113
- ? tr('auth.setPassword.welcomeTitle', `Welcome to ${appName}`, { appName })
114
- : tr('auth.setPassword.recoveryTitle', 'Choose a new password')
115
- const subtitle = invite
116
- ? tr('auth.setPassword.welcomeSubtitle', 'Choose a password to finish setting up your access.')
117
- : tr('auth.setPassword.recoverySubtitle', 'Set a new password to get back into your account.')
118
-
119
- async function submit(event: React.FormEvent) {
120
- event.preventDefault()
121
- setLocalError(null)
122
- if (password.length < MIN_LENGTH) {
123
- setLocalError(tr('auth.setPassword.tooShort', `Password must be at least ${MIN_LENGTH} characters.`))
124
- return
125
- }
126
- if (password !== confirmPassword) {
127
- setLocalError(tr('auth.setPassword.mismatch', 'Passwords do not match.'))
128
- return
129
- }
130
- setBusy(true)
131
- try {
132
- await updatePassword(password)
133
- onSuccess?.()
134
- } catch {
135
- // useAuth already surfaced it; keep the user on the form.
136
- } finally {
137
- setBusy(false)
138
- }
139
- }
140
-
141
- return (
142
- <div className="flex min-h-screen items-center justify-center bg-sidebar p-4">
143
- <div className="w-full max-w-md rounded-2xl border border-border/50 bg-card p-8 shadow-2xl sm:p-10">
144
- {logo && <div className="mb-6 flex justify-center">{logo}</div>}
145
- {checking ? (
146
- <p className="py-6 text-center text-sm text-muted-foreground">
147
- {tr('auth.setPassword.checking', 'Checking your link...')}
148
- </p>
149
- ) : expired ? (
150
- <>
151
- <div className="mb-6">
152
- <h1 className="text-2xl font-bold text-foreground">
153
- {tr('auth.setPassword.expiredTitle', 'This link has expired')}
154
- </h1>
155
- <p className="mt-1 text-sm text-muted-foreground">
156
- {tr(
157
- 'auth.setPassword.expiredSubtitle',
158
- 'Reset links can only be used once. Enter your e-mail and we will send a new one.',
159
- )}
160
- </p>
161
- </div>
162
- <ForgotPasswordForm
163
- initialEmail={user?.email}
164
- submitLabel={tr('auth.setPassword.sendNewLink', 'Send new link')}
165
- sendingLabel={tr('auth.setPassword.sending', 'Sending...')}
166
- sentLabel={tr('auth.setPassword.sent', 'Check your inbox — a new link is on the way.')}
167
- emailLabel={tr('auth.setPassword.email', 'E-mail')}
168
- backLabel={tr('auth.setPassword.backToLogin', 'Back to sign in')}
169
- onBackToLogin={onSuccess}
170
- />
171
- </>
172
- ) : (
173
- <>
174
- <div className="mb-6">
175
- <h1 className="text-2xl font-bold text-foreground">{title}</h1>
176
- <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
177
- </div>
178
- <form onSubmit={submit} className="space-y-4">
179
- <label className="block space-y-1.5 text-sm font-medium">
180
- <span>{tr('auth.setPassword.newPassword', 'New password')}</span>
181
- <input
182
- className={fieldClass()}
183
- type="password"
184
- autoComplete="new-password"
185
- autoFocus
186
- required
187
- value={password}
188
- onChange={(e) => setPassword(e.target.value)}
189
- disabled={disabled}
190
- />
191
- </label>
192
- <label className="block space-y-1.5 text-sm font-medium">
193
- <span>{tr('auth.setPassword.confirmPassword', 'Confirm password')}</span>
194
- <input
195
- className={fieldClass()}
196
- type="password"
197
- autoComplete="new-password"
198
- required
199
- value={confirmPassword}
200
- onChange={(e) => setConfirmPassword(e.target.value)}
201
- disabled={disabled}
202
- />
203
- </label>
204
- {(localError || error) && (
205
- <p className="text-sm text-destructive">{localError ?? error?.message}</p>
206
- )}
207
- <button className={buttonClass()} type="submit" disabled={disabled}>
208
- {disabled
209
- ? tr('auth.setPassword.saving', 'Saving...')
210
- : tr('auth.setPassword.submit', 'Save password')}
211
- </button>
212
- </form>
213
- {/* An invitee who does not want to continue needs a way out that does
214
- not leave them stuck on this screen with a live session. */}
215
- <button
216
- type="button"
217
- className="mt-4 w-full text-center text-sm text-muted-foreground hover:text-foreground"
218
- onClick={() => {
219
- void signOut().finally(() => onSuccess?.())
220
- }}
221
- >
222
- {tr('auth.setPassword.backToLogin', 'Back to sign in')}
223
- </button>
224
- </>
225
- )}
226
- </div>
227
- </div>
228
- )
229
- }
package/src/types.ts DELETED
@@ -1,65 +0,0 @@
1
- import type React from 'react'
2
- import type { AuthAdapter, AuthProvider } from '@fayz-ai/core'
3
- import type { SupabaseAuthConfig } from '@fayz-ai/auth'
4
-
5
- export type AuthProviderKey = 'supabase' | 'mock' | (string & {})
6
- export type AuthLayout = 'split' | 'centered'
7
-
8
- /** A product ambassador rendered as a floating circular photo bubble on the
9
- * split-login decorative panel. `image` may be a local path or a data URI. */
10
- export interface LoginAmbassador {
11
- name: string
12
- role?: string
13
- image: string
14
- }
15
-
16
- export interface AuthRoutesConfig {
17
- login?: string
18
- callback?: string
19
- resetPassword?: string
20
- afterSignIn?: string
21
- }
22
-
23
- export interface AuthOAuthConfig {
24
- enabled?: boolean
25
- providers?: Exclude<AuthProvider, 'email'>[]
26
- }
27
-
28
- export interface AuthSupabaseConfig {
29
- url?: string
30
- anonKey?: string
31
- client?: SupabaseAuthConfig['supabaseClient']
32
- callbackUrl?: string
33
- resetPasswordUrl?: string
34
- }
35
-
36
- export interface AuthPluginOptions {
37
- provider?: AuthProviderKey | AuthAdapter
38
- adapter?: AuthProviderKey | AuthAdapter
39
- supabase?: AuthSupabaseConfig
40
- requireAuth?: boolean
41
- layout?: AuthLayout
42
- logo?: React.ReactNode
43
- tagline?: string
44
- description?: string
45
- loginAmbassadors?: LoginAmbassador[]
46
- loginAmbassadorsLabel?: string
47
- oauth?: AuthOAuthConfig
48
- routes?: AuthRoutesConfig
49
- }
50
-
51
- export interface ResolvedAuthPlugin {
52
- kind: 'auth'
53
- adapter: AuthAdapter
54
- requireAuth: boolean
55
- layout: AuthLayout
56
- logo?: React.ReactNode
57
- tagline?: string
58
- description?: string
59
- loginAmbassadors?: LoginAmbassador[]
60
- loginAmbassadorsLabel?: string
61
- oauth: Required<AuthOAuthConfig>
62
- routes: Required<AuthRoutesConfig>
63
- }
64
-
65
- export type AuthFormView = 'login' | 'signup' | 'recovery'
@@ -1,48 +0,0 @@
1
- import { createContext, useContext, useState, type ReactNode } from 'react'
2
- import { PhoneAuthModal, type PhoneAuthModalConfig } from './PhoneAuthModal'
3
-
4
- interface AuthModalContextValue {
5
- openAuthModal: () => void
6
- closeAuthModal: () => void
7
- isAuthModalOpen: boolean
8
- }
9
-
10
- const AuthModalContext = createContext<AuthModalContextValue | null>(null)
11
-
12
- export interface AuthModalProviderProps {
13
- /** Enables the phone sign-in/up modal (default for dogfood sites). */
14
- phoneAuth?: boolean
15
- config?: PhoneAuthModalConfig
16
- children: ReactNode
17
- }
18
-
19
- /**
20
- * Mounts the default phone-auth login modal and exposes openAuthModal() to the
21
- * host (e.g. an "Acessar área do cliente" button). Render INSIDE <AuthProvider>
22
- * so the modal can sign the user in through the mounted adapter.
23
- */
24
- export function AuthModalProvider({ phoneAuth = true, config, children }: AuthModalProviderProps) {
25
- const [open, setOpen] = useState(false)
26
- const value: AuthModalContextValue = {
27
- openAuthModal: () => setOpen(true),
28
- closeAuthModal: () => setOpen(false),
29
- isAuthModalOpen: open,
30
- }
31
- return (
32
- <AuthModalContext.Provider value={value}>
33
- {children}
34
- {phoneAuth ? <PhoneAuthModal open={open} onClose={() => setOpen(false)} config={config} /> : null}
35
- </AuthModalContext.Provider>
36
- )
37
- }
38
-
39
- /** Access the auth modal controls. Returns a no-op opener if no provider is mounted. */
40
- export function useAuthModal(): AuthModalContextValue {
41
- return (
42
- useContext(AuthModalContext) ?? {
43
- openAuthModal: () => {},
44
- closeAuthModal: () => {},
45
- isAuthModalOpen: false,
46
- }
47
- )
48
- }
@@ -1,178 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import * as Dialog from '@radix-ui/react-dialog'
3
- import { X, Smartphone, ArrowLeft } from 'lucide-react'
4
- import { COUNTRIES, DEFAULT_COUNTRY, getCountry, maskPhone, unmaskPhone } from '@fayz-ai/core'
5
- import { useAuth } from '@fayz-ai/auth'
6
- import { phoneToEmail } from './phone'
7
-
8
- export interface PhoneAuthModalConfig {
9
- /** Business/brand name shown in the copy. */
10
- brand?: string
11
- title?: string
12
- }
13
-
14
- type Mode = 'signin' | 'signup'
15
-
16
- /**
17
- * Default phone sign-in / sign-up modal for dogfood Fayz sites. Built on Radix
18
- * Dialog (backdrop, focus-trap, Esc, scroll-lock, enter/exit animation via
19
- * tailwindcss-animate) — no reinvented modal chrome. Entrar / Criar conta
20
- * toggle, phone → WhatsApp code (POC bypass `0000`) → signs the user in via the
21
- * mounted auth adapter (phone = account). Token-only; render inside <AuthProvider>.
22
- */
23
- export function PhoneAuthModal({
24
- open, onClose, config,
25
- }: { open: boolean; onClose: () => void; config?: PhoneAuthModalConfig }) {
26
- const { signUp } = useAuth()
27
-
28
- const [mode, setMode] = useState<Mode>('signup')
29
- const [step, setStep] = useState<'phone' | 'code'>('phone')
30
- const [countryIso, setCountryIso] = useState(DEFAULT_COUNTRY)
31
- const country = getCountry(countryIso)
32
- const [phone, setPhone] = useState('')
33
- const [name, setName] = useState('')
34
- const [code, setCode] = useState('')
35
- const [codeError, setCodeError] = useState(false)
36
- const [busy, setBusy] = useState(false)
37
-
38
- // Fresh flow each time it opens.
39
- useEffect(() => {
40
- if (open) { setStep('phone'); setCode(''); setCodeError(false); setBusy(false) }
41
- }, [open])
42
-
43
- function sendCode() {
44
- if (unmaskPhone(phone).length < 8) return
45
- setCode(''); setCodeError(false); setStep('code')
46
- }
47
-
48
- async function confirmCode() {
49
- if (code.trim() !== '0000') { setCodeError(true); return }
50
- setBusy(true)
51
- try {
52
- await signUp(phoneToEmail(`${country.dial} ${phone}`), unmaskPhone(phone), name.trim() || 'Cliente')
53
- onClose()
54
- } catch {
55
- setCodeError(true)
56
- } finally {
57
- setBusy(false)
58
- }
59
- }
60
-
61
- const brandLabel = config?.brand ? ` na ${config.brand}` : ''
62
-
63
- return (
64
- <Dialog.Root open={open} onOpenChange={(o) => { if (!o) onClose() }}>
65
- <Dialog.Portal>
66
- <Dialog.Overlay className="fixed inset-0 z-[100] bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0" />
67
- <Dialog.Content
68
- onOpenAutoFocus={(e) => e.preventDefault()}
69
- className="fixed left-1/2 top-1/2 z-[101] w-[calc(100%-2rem)] max-w-[26rem] -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-border bg-card p-6 shadow-2xl duration-200 focus:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:slide-in-from-bottom-2 motion-reduce:animate-none"
70
- >
71
- {/* Header */}
72
- <div className="flex items-center gap-3">
73
- <span className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent text-primary">
74
- <Smartphone className="h-5 w-5" />
75
- </span>
76
- <div className="min-w-0">
77
- <Dialog.Title className="text-base font-semibold text-foreground">
78
- {config?.title ?? 'Acessar sua conta'}
79
- </Dialog.Title>
80
- <Dialog.Description className="text-xs text-muted-foreground">
81
- {mode === 'signup' ? `Crie sua conta${brandLabel} em segundos` : `Bem-vindo de volta${brandLabel}`}
82
- </Dialog.Description>
83
- </div>
84
- </div>
85
-
86
- <Dialog.Close
87
- aria-label="Fechar"
88
- className="absolute right-4 top-4 flex h-7 w-7 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
89
- >
90
- <X className="h-4 w-4" />
91
- </Dialog.Close>
92
-
93
- {/* Mode toggle (only on the phone step) */}
94
- {step === 'phone' ? (
95
- <div className="mt-5 grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 text-sm">
96
- {(['signin', 'signup'] as Mode[]).map((m) => (
97
- <button
98
- key={m}
99
- type="button"
100
- onClick={() => setMode(m)}
101
- className={`rounded-md py-1.5 font-medium transition-colors ${mode === m ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
102
- >
103
- {m === 'signin' ? 'Entrar' : 'Criar conta'}
104
- </button>
105
- ))}
106
- </div>
107
- ) : null}
108
-
109
- {step === 'phone' ? (
110
- <form onSubmit={(e) => { e.preventDefault(); sendCode() }} className="mt-4 space-y-3">
111
- {mode === 'signup' ? (
112
- <div>
113
- <label htmlFor="auth-name" className="mb-1 block text-sm font-medium text-foreground">Nome</label>
114
- <input
115
- id="auth-name" type="text" autoComplete="name" value={name}
116
- onChange={(e) => setName(e.target.value)}
117
- className="w-full rounded-xl border border-border bg-background px-3.5 py-2.5 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
118
- placeholder="Seu nome"
119
- />
120
- </div>
121
- ) : null}
122
- <div>
123
- <label htmlFor="auth-phone" className="mb-1 block text-sm font-medium text-foreground">Celular</label>
124
- <div className="flex">
125
- <select
126
- aria-label="País"
127
- value={countryIso}
128
- onChange={(e) => { const c = getCountry(e.target.value); setCountryIso(c.iso2); setPhone(maskPhone(unmaskPhone(phone), c.mask)) }}
129
- className="appearance-none rounded-l-xl border border-r-0 border-border bg-muted py-2.5 pl-3 pr-6 text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
130
- >
131
- {COUNTRIES.map((c) => <option key={c.iso2} value={c.iso2}>{c.flag} {c.dial}</option>)}
132
- </select>
133
- <input
134
- id="auth-phone" type="tel" autoComplete="tel" inputMode="tel" value={phone} autoFocus
135
- onChange={(e) => setPhone(maskPhone(e.target.value, country.mask))}
136
- className="w-full rounded-r-xl border border-border bg-background px-3.5 py-2.5 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
137
- placeholder={`Ex.: ${maskPhone('11987654321', country.mask)}`}
138
- />
139
- </div>
140
- </div>
141
- <button
142
- type="submit"
143
- disabled={unmaskPhone(phone).length < 8}
144
- className="w-full rounded-xl bg-primary py-2.5 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
145
- >
146
- Enviar código via WhatsApp
147
- </button>
148
- <p className="text-center text-xs text-muted-foreground">Enviaremos um código para confirmar seu número.</p>
149
- </form>
150
- ) : (
151
- <form onSubmit={(e) => { e.preventDefault(); void confirmCode() }} className="mt-5 space-y-3">
152
- <p className="text-sm text-muted-foreground">
153
- Enviamos um código via WhatsApp para <span className="font-medium text-foreground">{country.flag} {country.dial} {phone}</span>.
154
- </p>
155
- <input
156
- type="text" inputMode="numeric" value={code} autoFocus
157
- onChange={(e) => { setCode(e.target.value); setCodeError(false) }}
158
- className="w-full rounded-xl border border-border bg-background px-4 py-3 text-center text-lg tracking-[0.4em] text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring placeholder:text-sm placeholder:tracking-normal"
159
- placeholder="Digite o código"
160
- />
161
- {codeError ? <p role="alert" className="text-sm text-destructive">Código inválido. Para testar, use 0000.</p> : null}
162
- <button
163
- type="submit"
164
- disabled={busy}
165
- className="w-full rounded-xl bg-primary py-2.5 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
166
- >
167
- {busy ? 'Entrando…' : mode === 'signup' ? 'Criar conta e entrar' : 'Confirmar e entrar'}
168
- </button>
169
- <button type="button" onClick={() => setStep('phone')} className="mx-auto flex items-center gap-1 text-sm font-medium text-primary hover:underline">
170
- <ArrowLeft className="h-3.5 w-3.5" /> Trocar número
171
- </button>
172
- </form>
173
- )}
174
- </Dialog.Content>
175
- </Dialog.Portal>
176
- </Dialog.Root>
177
- )
178
- }
@@ -1,17 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @fayz-ai/plugin-auth/website — lean website auth surface (no @fayz-ai/ui).
3
- //
4
- // Ships the DEFAULT phone sign-in/sign-up modal for dogfood Fayz sites +
5
- // AuthModalProvider (openAuthModal control) + the canonical phone→email helper.
6
- // Re-exports the auth runtime from @fayz-ai/auth for one-stop host wiring.
7
- // ---------------------------------------------------------------------------
8
-
9
- export { AuthModalProvider, useAuthModal } from './AuthModalProvider'
10
- export type { AuthModalProviderProps } from './AuthModalProvider'
11
- export { PhoneAuthModal } from './PhoneAuthModal'
12
- export type { PhoneAuthModalConfig } from './PhoneAuthModal'
13
- export { phoneToEmail } from './phone'
14
-
15
- // Convenience re-exports so hosts import auth from one place.
16
- export { AuthProvider, useAuth, createMockAuthAdapter, createSupabaseAuthAdapter } from '@fayz-ai/auth'
17
- export type { AuthAdapter, AuthUser, AuthSession } from '@fayz-ai/core'
@@ -1,12 +0,0 @@
1
- import { unmaskPhone } from '@fayz-ai/core'
2
-
3
- /**
4
- * Canonical phone → auth email. Used by BOTH the phone-auth modal and any host
5
- * booking→auth bridge so the same number always maps to the same account
6
- * (Quaddro model: phone = identity). Pass the phone WITH its country dial so the
7
- * digits are globally unique.
8
- */
9
- export function phoneToEmail(phoneWithDial: string): string {
10
- const digits = unmaskPhone(phoneWithDial)
11
- return `${digits || 'anon'}@phone.fayz.local`
12
- }
File without changes
File without changes