@fayz-ai/plugin-auth 0.1.2 → 0.1.3

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.
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PhoneAuthModalConfig } from './PhoneAuthModal';
3
+ interface AuthModalContextValue {
4
+ openAuthModal: () => void;
5
+ closeAuthModal: () => void;
6
+ isAuthModalOpen: boolean;
7
+ }
8
+ export interface AuthModalProviderProps {
9
+ /** Enables the phone sign-in/up modal (default for dogfood sites). */
10
+ phoneAuth?: boolean;
11
+ config?: PhoneAuthModalConfig;
12
+ children: ReactNode;
13
+ }
14
+ /**
15
+ * Mounts the default phone-auth login modal and exposes openAuthModal() to the
16
+ * host (e.g. an "Acessar área do cliente" button). Render INSIDE <AuthProvider>
17
+ * so the modal can sign the user in through the mounted adapter.
18
+ */
19
+ export declare function AuthModalProvider({ phoneAuth, config, children }: AuthModalProviderProps): import("react").JSX.Element;
20
+ /** Access the auth modal controls. Returns a no-op opener if no provider is mounted. */
21
+ export declare function useAuthModal(): AuthModalContextValue;
22
+ export {};
23
+ //# sourceMappingURL=AuthModalProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthModalProvider.d.ts","sourceRoot":"","sources":["../../src/website/AuthModalProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3E,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE5E,UAAU,qBAAqB;IAC7B,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,eAAe,EAAE,OAAO,CAAA;CACzB;AAID,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,oBAAoB,CAAA;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,SAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAa/F;AAED,wFAAwF;AACxF,wBAAgB,YAAY,IAAI,qBAAqB,CAQpD"}
@@ -0,0 +1,18 @@
1
+ export interface PhoneAuthModalConfig {
2
+ /** Business/brand name shown in the copy. */
3
+ brand?: string;
4
+ title?: string;
5
+ }
6
+ /**
7
+ * Default phone sign-in / sign-up modal for dogfood Fayz sites. Built on Radix
8
+ * Dialog (backdrop, focus-trap, Esc, scroll-lock, enter/exit animation via
9
+ * tailwindcss-animate) — no reinvented modal chrome. Entrar / Criar conta
10
+ * toggle, phone → WhatsApp code (POC bypass `0000`) → signs the user in via the
11
+ * mounted auth adapter (phone = account). Token-only; render inside <AuthProvider>.
12
+ */
13
+ export declare function PhoneAuthModal({ open, onClose, config, }: {
14
+ open: boolean;
15
+ onClose: () => void;
16
+ config?: PhoneAuthModalConfig;
17
+ }): import("react").JSX.Element;
18
+ //# sourceMappingURL=PhoneAuthModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhoneAuthModal.d.ts","sourceRoot":"","sources":["../../src/website/PhoneAuthModal.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,oBAAoB;IACnC,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EAAE,OAAO,EAAE,MAAM,GACtB,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,oBAAoB,CAAA;CAAE,+BAyJvE"}
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var Dialog = require('@radix-ui/react-dialog');
5
+ var lucideReact = require('lucide-react');
6
+ var core = require('@fayz-ai/core');
7
+ var auth = require('@fayz-ai/auth');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var Dialog__namespace = /*#__PURE__*/_interopNamespace(Dialog);
29
+
30
+ // src/website/AuthModalProvider.tsx
31
+ function phoneToEmail(phoneWithDial) {
32
+ const digits = core.unmaskPhone(phoneWithDial);
33
+ return `${digits || "anon"}@phone.fayz.local`;
34
+ }
35
+ function PhoneAuthModal({
36
+ open,
37
+ onClose,
38
+ config
39
+ }) {
40
+ const { signUp } = auth.useAuth();
41
+ const [mode, setMode] = react.useState("signup");
42
+ const [step, setStep] = react.useState("phone");
43
+ const [countryIso, setCountryIso] = react.useState(core.DEFAULT_COUNTRY);
44
+ const country = core.getCountry(countryIso);
45
+ const [phone, setPhone] = react.useState("");
46
+ const [name, setName] = react.useState("");
47
+ const [code, setCode] = react.useState("");
48
+ const [codeError, setCodeError] = react.useState(false);
49
+ const [busy, setBusy] = react.useState(false);
50
+ react.useEffect(() => {
51
+ if (open) {
52
+ setStep("phone");
53
+ setCode("");
54
+ setCodeError(false);
55
+ setBusy(false);
56
+ }
57
+ }, [open]);
58
+ function sendCode() {
59
+ if (core.unmaskPhone(phone).length < 8) return;
60
+ setCode("");
61
+ setCodeError(false);
62
+ setStep("code");
63
+ }
64
+ async function confirmCode() {
65
+ if (code.trim() !== "0000") {
66
+ setCodeError(true);
67
+ return;
68
+ }
69
+ setBusy(true);
70
+ try {
71
+ await signUp(phoneToEmail(`${country.dial} ${phone}`), core.unmaskPhone(phone), name.trim() || "Cliente");
72
+ onClose();
73
+ } catch {
74
+ setCodeError(true);
75
+ } finally {
76
+ setBusy(false);
77
+ }
78
+ }
79
+ const brandLabel = config?.brand ? ` na ${config.brand}` : "";
80
+ return /* @__PURE__ */ jsxRuntime.jsx(Dialog__namespace.Root, { open, onOpenChange: (o) => {
81
+ if (!o) onClose();
82
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(Dialog__namespace.Portal, { children: [
83
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog__namespace.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" }),
84
+ /* @__PURE__ */ jsxRuntime.jsxs(
85
+ Dialog__namespace.Content,
86
+ {
87
+ onOpenAutoFocus: (e) => e.preventDefault(),
88
+ 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",
89
+ children: [
90
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
91
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-accent text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Smartphone, { className: "h-5 w-5" }) }),
92
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
93
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog__namespace.Title, { className: "text-base font-semibold text-foreground", children: config?.title ?? "Acessar sua conta" }),
94
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog__namespace.Description, { className: "text-xs text-muted-foreground", children: mode === "signup" ? `Crie sua conta${brandLabel} em segundos` : `Bem-vindo de volta${brandLabel}` })
95
+ ] })
96
+ ] }),
97
+ /* @__PURE__ */ jsxRuntime.jsx(
98
+ Dialog__namespace.Close,
99
+ {
100
+ "aria-label": "Fechar",
101
+ 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",
102
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" })
103
+ }
104
+ ),
105
+ step === "phone" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-5 grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 text-sm", children: ["signin", "signup"].map((m) => /* @__PURE__ */ jsxRuntime.jsx(
106
+ "button",
107
+ {
108
+ type: "button",
109
+ onClick: () => setMode(m),
110
+ className: `rounded-md py-1.5 font-medium transition-colors ${mode === m ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
111
+ children: m === "signin" ? "Entrar" : "Criar conta"
112
+ },
113
+ m
114
+ )) }) : null,
115
+ step === "phone" ? /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: (e) => {
116
+ e.preventDefault();
117
+ sendCode();
118
+ }, className: "mt-4 space-y-3", children: [
119
+ mode === "signup" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
120
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "auth-name", className: "mb-1 block text-sm font-medium text-foreground", children: "Nome" }),
121
+ /* @__PURE__ */ jsxRuntime.jsx(
122
+ "input",
123
+ {
124
+ id: "auth-name",
125
+ type: "text",
126
+ autoComplete: "name",
127
+ value: name,
128
+ onChange: (e) => setName(e.target.value),
129
+ 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",
130
+ placeholder: "Seu nome"
131
+ }
132
+ )
133
+ ] }) : null,
134
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "auth-phone", className: "mb-1 block text-sm font-medium text-foreground", children: "Celular" }),
136
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
137
+ /* @__PURE__ */ jsxRuntime.jsx(
138
+ "select",
139
+ {
140
+ "aria-label": "Pa\xEDs",
141
+ value: countryIso,
142
+ onChange: (e) => {
143
+ const c = core.getCountry(e.target.value);
144
+ setCountryIso(c.iso2);
145
+ setPhone(core.maskPhone(core.unmaskPhone(phone), c.mask));
146
+ },
147
+ 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",
148
+ children: core.COUNTRIES.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: c.iso2, children: [
149
+ c.flag,
150
+ " ",
151
+ c.dial
152
+ ] }, c.iso2))
153
+ }
154
+ ),
155
+ /* @__PURE__ */ jsxRuntime.jsx(
156
+ "input",
157
+ {
158
+ id: "auth-phone",
159
+ type: "tel",
160
+ autoComplete: "tel",
161
+ inputMode: "tel",
162
+ value: phone,
163
+ autoFocus: true,
164
+ onChange: (e) => setPhone(core.maskPhone(e.target.value, country.mask)),
165
+ 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",
166
+ placeholder: `Ex.: ${core.maskPhone("11987654321", country.mask)}`
167
+ }
168
+ )
169
+ ] })
170
+ ] }),
171
+ /* @__PURE__ */ jsxRuntime.jsx(
172
+ "button",
173
+ {
174
+ type: "submit",
175
+ disabled: core.unmaskPhone(phone).length < 8,
176
+ 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",
177
+ children: "Enviar c\xF3digo via WhatsApp"
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-xs text-muted-foreground", children: "Enviaremos um c\xF3digo para confirmar seu n\xFAmero." })
181
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: (e) => {
182
+ e.preventDefault();
183
+ void confirmCode();
184
+ }, className: "mt-5 space-y-3", children: [
185
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
186
+ "Enviamos um c\xF3digo via WhatsApp para ",
187
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-foreground", children: [
188
+ country.flag,
189
+ " ",
190
+ country.dial,
191
+ " ",
192
+ phone
193
+ ] }),
194
+ "."
195
+ ] }),
196
+ /* @__PURE__ */ jsxRuntime.jsx(
197
+ "input",
198
+ {
199
+ type: "text",
200
+ inputMode: "numeric",
201
+ value: code,
202
+ autoFocus: true,
203
+ onChange: (e) => {
204
+ setCode(e.target.value);
205
+ setCodeError(false);
206
+ },
207
+ 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",
208
+ placeholder: "Digite o c\xF3digo"
209
+ }
210
+ ),
211
+ codeError ? /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-sm text-destructive", children: "C\xF3digo inv\xE1lido. Para testar, use 0000." }) : null,
212
+ /* @__PURE__ */ jsxRuntime.jsx(
213
+ "button",
214
+ {
215
+ type: "submit",
216
+ disabled: busy,
217
+ 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",
218
+ children: busy ? "Entrando\u2026" : mode === "signup" ? "Criar conta e entrar" : "Confirmar e entrar"
219
+ }
220
+ ),
221
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => setStep("phone"), className: "mx-auto flex items-center gap-1 text-sm font-medium text-primary hover:underline", children: [
222
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-3.5 w-3.5" }),
223
+ " Trocar n\xFAmero"
224
+ ] })
225
+ ] })
226
+ ]
227
+ }
228
+ )
229
+ ] }) });
230
+ }
231
+ var AuthModalContext = react.createContext(null);
232
+ function AuthModalProvider({ phoneAuth = true, config, children }) {
233
+ const [open, setOpen] = react.useState(false);
234
+ const value = {
235
+ openAuthModal: () => setOpen(true),
236
+ closeAuthModal: () => setOpen(false),
237
+ isAuthModalOpen: open
238
+ };
239
+ return /* @__PURE__ */ jsxRuntime.jsxs(AuthModalContext.Provider, { value, children: [
240
+ children,
241
+ phoneAuth ? /* @__PURE__ */ jsxRuntime.jsx(PhoneAuthModal, { open, onClose: () => setOpen(false), config }) : null
242
+ ] });
243
+ }
244
+ function useAuthModal() {
245
+ return react.useContext(AuthModalContext) ?? {
246
+ openAuthModal: () => {
247
+ },
248
+ closeAuthModal: () => {
249
+ },
250
+ isAuthModalOpen: false
251
+ };
252
+ }
253
+
254
+ Object.defineProperty(exports, "AuthProvider", {
255
+ enumerable: true,
256
+ get: function () { return auth.AuthProvider; }
257
+ });
258
+ Object.defineProperty(exports, "createMockAuthAdapter", {
259
+ enumerable: true,
260
+ get: function () { return auth.createMockAuthAdapter; }
261
+ });
262
+ Object.defineProperty(exports, "createSupabaseAuthAdapter", {
263
+ enumerable: true,
264
+ get: function () { return auth.createSupabaseAuthAdapter; }
265
+ });
266
+ Object.defineProperty(exports, "useAuth", {
267
+ enumerable: true,
268
+ get: function () { return auth.useAuth; }
269
+ });
270
+ exports.AuthModalProvider = AuthModalProvider;
271
+ exports.PhoneAuthModal = PhoneAuthModal;
272
+ exports.phoneToEmail = phoneToEmail;
273
+ exports.useAuthModal = useAuthModal;
274
+ //# sourceMappingURL=index.cjs.map
275
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/website/phone.ts","../../src/website/PhoneAuthModal.tsx","../../src/website/AuthModalProvider.tsx"],"names":["unmaskPhone","useAuth","useState","DEFAULT_COUNTRY","getCountry","useEffect","Dialog","jsxs","jsx","Smartphone","X","maskPhone","COUNTRIES","ArrowLeft","createContext","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { unmaskPhone } from '@fayz-ai/core'\n\n/**\n * Canonical phone → auth email. Used by BOTH the phone-auth modal and any host\n * booking→auth bridge so the same number always maps to the same account\n * (Quaddro model: phone = identity). Pass the phone WITH its country dial so the\n * digits are globally unique.\n */\nexport function phoneToEmail(phoneWithDial: string): string {\n const digits = unmaskPhone(phoneWithDial)\n return `${digits || 'anon'}@phone.fayz.local`\n}\n","import { useEffect, useState } from 'react'\nimport * as Dialog from '@radix-ui/react-dialog'\nimport { X, Smartphone, ArrowLeft } from 'lucide-react'\nimport { COUNTRIES, DEFAULT_COUNTRY, getCountry, maskPhone, unmaskPhone } from '@fayz-ai/core'\nimport { useAuth } from '@fayz-ai/auth'\nimport { phoneToEmail } from './phone'\n\nexport interface PhoneAuthModalConfig {\n /** Business/brand name shown in the copy. */\n brand?: string\n title?: string\n}\n\ntype Mode = 'signin' | 'signup'\n\n/**\n * Default phone sign-in / sign-up modal for dogfood Fayz sites. Built on Radix\n * Dialog (backdrop, focus-trap, Esc, scroll-lock, enter/exit animation via\n * tailwindcss-animate) — no reinvented modal chrome. Entrar / Criar conta\n * toggle, phone → WhatsApp code (POC bypass `0000`) → signs the user in via the\n * mounted auth adapter (phone = account). Token-only; render inside <AuthProvider>.\n */\nexport function PhoneAuthModal({\n open, onClose, config,\n}: { open: boolean; onClose: () => void; config?: PhoneAuthModalConfig }) {\n const { signUp } = useAuth()\n\n const [mode, setMode] = useState<Mode>('signup')\n const [step, setStep] = useState<'phone' | 'code'>('phone')\n const [countryIso, setCountryIso] = useState(DEFAULT_COUNTRY)\n const country = getCountry(countryIso)\n const [phone, setPhone] = useState('')\n const [name, setName] = useState('')\n const [code, setCode] = useState('')\n const [codeError, setCodeError] = useState(false)\n const [busy, setBusy] = useState(false)\n\n // Fresh flow each time it opens.\n useEffect(() => {\n if (open) { setStep('phone'); setCode(''); setCodeError(false); setBusy(false) }\n }, [open])\n\n function sendCode() {\n if (unmaskPhone(phone).length < 8) return\n setCode(''); setCodeError(false); setStep('code')\n }\n\n async function confirmCode() {\n if (code.trim() !== '0000') { setCodeError(true); return }\n setBusy(true)\n try {\n await signUp(phoneToEmail(`${country.dial} ${phone}`), unmaskPhone(phone), name.trim() || 'Cliente')\n onClose()\n } catch {\n setCodeError(true)\n } finally {\n setBusy(false)\n }\n }\n\n const brandLabel = config?.brand ? ` na ${config.brand}` : ''\n\n return (\n <Dialog.Root open={open} onOpenChange={(o) => { if (!o) onClose() }}>\n <Dialog.Portal>\n <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\" />\n <Dialog.Content\n onOpenAutoFocus={(e) => e.preventDefault()}\n 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\"\n >\n {/* Header */}\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-accent text-primary\">\n <Smartphone className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <Dialog.Title className=\"text-base font-semibold text-foreground\">\n {config?.title ?? 'Acessar sua conta'}\n </Dialog.Title>\n <Dialog.Description className=\"text-xs text-muted-foreground\">\n {mode === 'signup' ? `Crie sua conta${brandLabel} em segundos` : `Bem-vindo de volta${brandLabel}`}\n </Dialog.Description>\n </div>\n </div>\n\n <Dialog.Close\n aria-label=\"Fechar\"\n 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\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n\n {/* Mode toggle (only on the phone step) */}\n {step === 'phone' ? (\n <div className=\"mt-5 grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 text-sm\">\n {(['signin', 'signup'] as Mode[]).map((m) => (\n <button\n key={m}\n type=\"button\"\n onClick={() => setMode(m)}\n className={`rounded-md py-1.5 font-medium transition-colors ${mode === m ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}\n >\n {m === 'signin' ? 'Entrar' : 'Criar conta'}\n </button>\n ))}\n </div>\n ) : null}\n\n {step === 'phone' ? (\n <form onSubmit={(e) => { e.preventDefault(); sendCode() }} className=\"mt-4 space-y-3\">\n {mode === 'signup' ? (\n <div>\n <label htmlFor=\"auth-name\" className=\"mb-1 block text-sm font-medium text-foreground\">Nome</label>\n <input\n id=\"auth-name\" type=\"text\" autoComplete=\"name\" value={name}\n onChange={(e) => setName(e.target.value)}\n 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\"\n placeholder=\"Seu nome\"\n />\n </div>\n ) : null}\n <div>\n <label htmlFor=\"auth-phone\" className=\"mb-1 block text-sm font-medium text-foreground\">Celular</label>\n <div className=\"flex\">\n <select\n aria-label=\"País\"\n value={countryIso}\n onChange={(e) => { const c = getCountry(e.target.value); setCountryIso(c.iso2); setPhone(maskPhone(unmaskPhone(phone), c.mask)) }}\n 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\"\n >\n {COUNTRIES.map((c) => <option key={c.iso2} value={c.iso2}>{c.flag} {c.dial}</option>)}\n </select>\n <input\n id=\"auth-phone\" type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" value={phone} autoFocus\n onChange={(e) => setPhone(maskPhone(e.target.value, country.mask))}\n 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\"\n placeholder={`Ex.: ${maskPhone('11987654321', country.mask)}`}\n />\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={unmaskPhone(phone).length < 8}\n 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\"\n >\n Enviar código via WhatsApp\n </button>\n <p className=\"text-center text-xs text-muted-foreground\">Enviaremos um código para confirmar seu número.</p>\n </form>\n ) : (\n <form onSubmit={(e) => { e.preventDefault(); void confirmCode() }} className=\"mt-5 space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n Enviamos um código via WhatsApp para <span className=\"font-medium text-foreground\">{country.flag} {country.dial} {phone}</span>.\n </p>\n <input\n type=\"text\" inputMode=\"numeric\" value={code} autoFocus\n onChange={(e) => { setCode(e.target.value); setCodeError(false) }}\n 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\"\n placeholder=\"Digite o código\"\n />\n {codeError ? <p role=\"alert\" className=\"text-sm text-destructive\">Código inválido. Para testar, use 0000.</p> : null}\n <button\n type=\"submit\"\n disabled={busy}\n 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\"\n >\n {busy ? 'Entrando…' : mode === 'signup' ? 'Criar conta e entrar' : 'Confirmar e entrar'}\n </button>\n <button type=\"button\" onClick={() => setStep('phone')} className=\"mx-auto flex items-center gap-1 text-sm font-medium text-primary hover:underline\">\n <ArrowLeft className=\"h-3.5 w-3.5\" /> Trocar número\n </button>\n </form>\n )}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n )\n}\n","import { createContext, useContext, useState, type ReactNode } from 'react'\nimport { PhoneAuthModal, type PhoneAuthModalConfig } from './PhoneAuthModal'\n\ninterface AuthModalContextValue {\n openAuthModal: () => void\n closeAuthModal: () => void\n isAuthModalOpen: boolean\n}\n\nconst AuthModalContext = createContext<AuthModalContextValue | null>(null)\n\nexport interface AuthModalProviderProps {\n /** Enables the phone sign-in/up modal (default for dogfood sites). */\n phoneAuth?: boolean\n config?: PhoneAuthModalConfig\n children: ReactNode\n}\n\n/**\n * Mounts the default phone-auth login modal and exposes openAuthModal() to the\n * host (e.g. an \"Acessar área do cliente\" button). Render INSIDE <AuthProvider>\n * so the modal can sign the user in through the mounted adapter.\n */\nexport function AuthModalProvider({ phoneAuth = true, config, children }: AuthModalProviderProps) {\n const [open, setOpen] = useState(false)\n const value: AuthModalContextValue = {\n openAuthModal: () => setOpen(true),\n closeAuthModal: () => setOpen(false),\n isAuthModalOpen: open,\n }\n return (\n <AuthModalContext.Provider value={value}>\n {children}\n {phoneAuth ? <PhoneAuthModal open={open} onClose={() => setOpen(false)} config={config} /> : null}\n </AuthModalContext.Provider>\n )\n}\n\n/** Access the auth modal controls. Returns a no-op opener if no provider is mounted. */\nexport function useAuthModal(): AuthModalContextValue {\n return (\n useContext(AuthModalContext) ?? {\n openAuthModal: () => {},\n closeAuthModal: () => {},\n isAuthModalOpen: false,\n }\n )\n}\n"]}
@@ -0,0 +1,8 @@
1
+ export { AuthModalProvider, useAuthModal } from './AuthModalProvider';
2
+ export type { AuthModalProviderProps } from './AuthModalProvider';
3
+ export { PhoneAuthModal } from './PhoneAuthModal';
4
+ export type { PhoneAuthModalConfig } from './PhoneAuthModal';
5
+ export { phoneToEmail } from './phone';
6
+ export { AuthProvider, useAuth, createMockAuthAdapter, createSupabaseAuthAdapter } from '@fayz-ai/auth';
7
+ export type { AuthAdapter, AuthUser, AuthSession } from '@fayz-ai/core';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/website/index.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACrE,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGtC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AACvG,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
@@ -0,0 +1,235 @@
1
+ import { createContext, useState, useEffect, useContext } from 'react';
2
+ import * as Dialog from '@radix-ui/react-dialog';
3
+ import { Smartphone, X, ArrowLeft } from 'lucide-react';
4
+ import { unmaskPhone, DEFAULT_COUNTRY, getCountry, COUNTRIES, maskPhone } from '@fayz-ai/core';
5
+ import { useAuth } from '@fayz-ai/auth';
6
+ export { AuthProvider, createMockAuthAdapter, createSupabaseAuthAdapter, useAuth } from '@fayz-ai/auth';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+
9
+ // src/website/AuthModalProvider.tsx
10
+ function phoneToEmail(phoneWithDial) {
11
+ const digits = unmaskPhone(phoneWithDial);
12
+ return `${digits || "anon"}@phone.fayz.local`;
13
+ }
14
+ function PhoneAuthModal({
15
+ open,
16
+ onClose,
17
+ config
18
+ }) {
19
+ const { signUp } = useAuth();
20
+ const [mode, setMode] = useState("signup");
21
+ const [step, setStep] = useState("phone");
22
+ const [countryIso, setCountryIso] = useState(DEFAULT_COUNTRY);
23
+ const country = getCountry(countryIso);
24
+ const [phone, setPhone] = useState("");
25
+ const [name, setName] = useState("");
26
+ const [code, setCode] = useState("");
27
+ const [codeError, setCodeError] = useState(false);
28
+ const [busy, setBusy] = useState(false);
29
+ useEffect(() => {
30
+ if (open) {
31
+ setStep("phone");
32
+ setCode("");
33
+ setCodeError(false);
34
+ setBusy(false);
35
+ }
36
+ }, [open]);
37
+ function sendCode() {
38
+ if (unmaskPhone(phone).length < 8) return;
39
+ setCode("");
40
+ setCodeError(false);
41
+ setStep("code");
42
+ }
43
+ async function confirmCode() {
44
+ if (code.trim() !== "0000") {
45
+ setCodeError(true);
46
+ return;
47
+ }
48
+ setBusy(true);
49
+ try {
50
+ await signUp(phoneToEmail(`${country.dial} ${phone}`), unmaskPhone(phone), name.trim() || "Cliente");
51
+ onClose();
52
+ } catch {
53
+ setCodeError(true);
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ }
58
+ const brandLabel = config?.brand ? ` na ${config.brand}` : "";
59
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange: (o) => {
60
+ if (!o) onClose();
61
+ }, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
62
+ /* @__PURE__ */ jsx(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" }),
63
+ /* @__PURE__ */ jsxs(
64
+ Dialog.Content,
65
+ {
66
+ onOpenAutoFocus: (e) => e.preventDefault(),
67
+ 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",
68
+ children: [
69
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
70
+ /* @__PURE__ */ jsx("span", { className: "flex h-10 w-10 items-center justify-center rounded-xl bg-accent text-primary", children: /* @__PURE__ */ jsx(Smartphone, { className: "h-5 w-5" }) }),
71
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
72
+ /* @__PURE__ */ jsx(Dialog.Title, { className: "text-base font-semibold text-foreground", children: config?.title ?? "Acessar sua conta" }),
73
+ /* @__PURE__ */ jsx(Dialog.Description, { className: "text-xs text-muted-foreground", children: mode === "signup" ? `Crie sua conta${brandLabel} em segundos` : `Bem-vindo de volta${brandLabel}` })
74
+ ] })
75
+ ] }),
76
+ /* @__PURE__ */ jsx(
77
+ Dialog.Close,
78
+ {
79
+ "aria-label": "Fechar",
80
+ 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",
81
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
82
+ }
83
+ ),
84
+ step === "phone" ? /* @__PURE__ */ jsx("div", { className: "mt-5 grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 text-sm", children: ["signin", "signup"].map((m) => /* @__PURE__ */ jsx(
85
+ "button",
86
+ {
87
+ type: "button",
88
+ onClick: () => setMode(m),
89
+ className: `rounded-md py-1.5 font-medium transition-colors ${mode === m ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
90
+ children: m === "signin" ? "Entrar" : "Criar conta"
91
+ },
92
+ m
93
+ )) }) : null,
94
+ step === "phone" ? /* @__PURE__ */ jsxs("form", { onSubmit: (e) => {
95
+ e.preventDefault();
96
+ sendCode();
97
+ }, className: "mt-4 space-y-3", children: [
98
+ mode === "signup" ? /* @__PURE__ */ jsxs("div", { children: [
99
+ /* @__PURE__ */ jsx("label", { htmlFor: "auth-name", className: "mb-1 block text-sm font-medium text-foreground", children: "Nome" }),
100
+ /* @__PURE__ */ jsx(
101
+ "input",
102
+ {
103
+ id: "auth-name",
104
+ type: "text",
105
+ autoComplete: "name",
106
+ value: name,
107
+ onChange: (e) => setName(e.target.value),
108
+ 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",
109
+ placeholder: "Seu nome"
110
+ }
111
+ )
112
+ ] }) : null,
113
+ /* @__PURE__ */ jsxs("div", { children: [
114
+ /* @__PURE__ */ jsx("label", { htmlFor: "auth-phone", className: "mb-1 block text-sm font-medium text-foreground", children: "Celular" }),
115
+ /* @__PURE__ */ jsxs("div", { className: "flex", children: [
116
+ /* @__PURE__ */ jsx(
117
+ "select",
118
+ {
119
+ "aria-label": "Pa\xEDs",
120
+ value: countryIso,
121
+ onChange: (e) => {
122
+ const c = getCountry(e.target.value);
123
+ setCountryIso(c.iso2);
124
+ setPhone(maskPhone(unmaskPhone(phone), c.mask));
125
+ },
126
+ 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",
127
+ children: COUNTRIES.map((c) => /* @__PURE__ */ jsxs("option", { value: c.iso2, children: [
128
+ c.flag,
129
+ " ",
130
+ c.dial
131
+ ] }, c.iso2))
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx(
135
+ "input",
136
+ {
137
+ id: "auth-phone",
138
+ type: "tel",
139
+ autoComplete: "tel",
140
+ inputMode: "tel",
141
+ value: phone,
142
+ autoFocus: true,
143
+ onChange: (e) => setPhone(maskPhone(e.target.value, country.mask)),
144
+ 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",
145
+ placeholder: `Ex.: ${maskPhone("11987654321", country.mask)}`
146
+ }
147
+ )
148
+ ] })
149
+ ] }),
150
+ /* @__PURE__ */ jsx(
151
+ "button",
152
+ {
153
+ type: "submit",
154
+ disabled: unmaskPhone(phone).length < 8,
155
+ 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",
156
+ children: "Enviar c\xF3digo via WhatsApp"
157
+ }
158
+ ),
159
+ /* @__PURE__ */ jsx("p", { className: "text-center text-xs text-muted-foreground", children: "Enviaremos um c\xF3digo para confirmar seu n\xFAmero." })
160
+ ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: (e) => {
161
+ e.preventDefault();
162
+ void confirmCode();
163
+ }, className: "mt-5 space-y-3", children: [
164
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
165
+ "Enviamos um c\xF3digo via WhatsApp para ",
166
+ /* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
167
+ country.flag,
168
+ " ",
169
+ country.dial,
170
+ " ",
171
+ phone
172
+ ] }),
173
+ "."
174
+ ] }),
175
+ /* @__PURE__ */ jsx(
176
+ "input",
177
+ {
178
+ type: "text",
179
+ inputMode: "numeric",
180
+ value: code,
181
+ autoFocus: true,
182
+ onChange: (e) => {
183
+ setCode(e.target.value);
184
+ setCodeError(false);
185
+ },
186
+ 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",
187
+ placeholder: "Digite o c\xF3digo"
188
+ }
189
+ ),
190
+ codeError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: "C\xF3digo inv\xE1lido. Para testar, use 0000." }) : null,
191
+ /* @__PURE__ */ jsx(
192
+ "button",
193
+ {
194
+ type: "submit",
195
+ disabled: busy,
196
+ 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",
197
+ children: busy ? "Entrando\u2026" : mode === "signup" ? "Criar conta e entrar" : "Confirmar e entrar"
198
+ }
199
+ ),
200
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setStep("phone"), className: "mx-auto flex items-center gap-1 text-sm font-medium text-primary hover:underline", children: [
201
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" }),
202
+ " Trocar n\xFAmero"
203
+ ] })
204
+ ] })
205
+ ]
206
+ }
207
+ )
208
+ ] }) });
209
+ }
210
+ var AuthModalContext = createContext(null);
211
+ function AuthModalProvider({ phoneAuth = true, config, children }) {
212
+ const [open, setOpen] = useState(false);
213
+ const value = {
214
+ openAuthModal: () => setOpen(true),
215
+ closeAuthModal: () => setOpen(false),
216
+ isAuthModalOpen: open
217
+ };
218
+ return /* @__PURE__ */ jsxs(AuthModalContext.Provider, { value, children: [
219
+ children,
220
+ phoneAuth ? /* @__PURE__ */ jsx(PhoneAuthModal, { open, onClose: () => setOpen(false), config }) : null
221
+ ] });
222
+ }
223
+ function useAuthModal() {
224
+ return useContext(AuthModalContext) ?? {
225
+ openAuthModal: () => {
226
+ },
227
+ closeAuthModal: () => {
228
+ },
229
+ isAuthModalOpen: false
230
+ };
231
+ }
232
+
233
+ export { AuthModalProvider, PhoneAuthModal, phoneToEmail, useAuthModal };
234
+ //# sourceMappingURL=index.js.map
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/website/phone.ts","../../src/website/PhoneAuthModal.tsx","../../src/website/AuthModalProvider.tsx"],"names":["unmaskPhone","useState","jsxs","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { unmaskPhone } from '@fayz-ai/core'\n\n/**\n * Canonical phone → auth email. Used by BOTH the phone-auth modal and any host\n * booking→auth bridge so the same number always maps to the same account\n * (Quaddro model: phone = identity). Pass the phone WITH its country dial so the\n * digits are globally unique.\n */\nexport function phoneToEmail(phoneWithDial: string): string {\n const digits = unmaskPhone(phoneWithDial)\n return `${digits || 'anon'}@phone.fayz.local`\n}\n","import { useEffect, useState } from 'react'\nimport * as Dialog from '@radix-ui/react-dialog'\nimport { X, Smartphone, ArrowLeft } from 'lucide-react'\nimport { COUNTRIES, DEFAULT_COUNTRY, getCountry, maskPhone, unmaskPhone } from '@fayz-ai/core'\nimport { useAuth } from '@fayz-ai/auth'\nimport { phoneToEmail } from './phone'\n\nexport interface PhoneAuthModalConfig {\n /** Business/brand name shown in the copy. */\n brand?: string\n title?: string\n}\n\ntype Mode = 'signin' | 'signup'\n\n/**\n * Default phone sign-in / sign-up modal for dogfood Fayz sites. Built on Radix\n * Dialog (backdrop, focus-trap, Esc, scroll-lock, enter/exit animation via\n * tailwindcss-animate) — no reinvented modal chrome. Entrar / Criar conta\n * toggle, phone → WhatsApp code (POC bypass `0000`) → signs the user in via the\n * mounted auth adapter (phone = account). Token-only; render inside <AuthProvider>.\n */\nexport function PhoneAuthModal({\n open, onClose, config,\n}: { open: boolean; onClose: () => void; config?: PhoneAuthModalConfig }) {\n const { signUp } = useAuth()\n\n const [mode, setMode] = useState<Mode>('signup')\n const [step, setStep] = useState<'phone' | 'code'>('phone')\n const [countryIso, setCountryIso] = useState(DEFAULT_COUNTRY)\n const country = getCountry(countryIso)\n const [phone, setPhone] = useState('')\n const [name, setName] = useState('')\n const [code, setCode] = useState('')\n const [codeError, setCodeError] = useState(false)\n const [busy, setBusy] = useState(false)\n\n // Fresh flow each time it opens.\n useEffect(() => {\n if (open) { setStep('phone'); setCode(''); setCodeError(false); setBusy(false) }\n }, [open])\n\n function sendCode() {\n if (unmaskPhone(phone).length < 8) return\n setCode(''); setCodeError(false); setStep('code')\n }\n\n async function confirmCode() {\n if (code.trim() !== '0000') { setCodeError(true); return }\n setBusy(true)\n try {\n await signUp(phoneToEmail(`${country.dial} ${phone}`), unmaskPhone(phone), name.trim() || 'Cliente')\n onClose()\n } catch {\n setCodeError(true)\n } finally {\n setBusy(false)\n }\n }\n\n const brandLabel = config?.brand ? ` na ${config.brand}` : ''\n\n return (\n <Dialog.Root open={open} onOpenChange={(o) => { if (!o) onClose() }}>\n <Dialog.Portal>\n <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\" />\n <Dialog.Content\n onOpenAutoFocus={(e) => e.preventDefault()}\n 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\"\n >\n {/* Header */}\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-accent text-primary\">\n <Smartphone className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <Dialog.Title className=\"text-base font-semibold text-foreground\">\n {config?.title ?? 'Acessar sua conta'}\n </Dialog.Title>\n <Dialog.Description className=\"text-xs text-muted-foreground\">\n {mode === 'signup' ? `Crie sua conta${brandLabel} em segundos` : `Bem-vindo de volta${brandLabel}`}\n </Dialog.Description>\n </div>\n </div>\n\n <Dialog.Close\n aria-label=\"Fechar\"\n 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\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n\n {/* Mode toggle (only on the phone step) */}\n {step === 'phone' ? (\n <div className=\"mt-5 grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 text-sm\">\n {(['signin', 'signup'] as Mode[]).map((m) => (\n <button\n key={m}\n type=\"button\"\n onClick={() => setMode(m)}\n className={`rounded-md py-1.5 font-medium transition-colors ${mode === m ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}\n >\n {m === 'signin' ? 'Entrar' : 'Criar conta'}\n </button>\n ))}\n </div>\n ) : null}\n\n {step === 'phone' ? (\n <form onSubmit={(e) => { e.preventDefault(); sendCode() }} className=\"mt-4 space-y-3\">\n {mode === 'signup' ? (\n <div>\n <label htmlFor=\"auth-name\" className=\"mb-1 block text-sm font-medium text-foreground\">Nome</label>\n <input\n id=\"auth-name\" type=\"text\" autoComplete=\"name\" value={name}\n onChange={(e) => setName(e.target.value)}\n 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\"\n placeholder=\"Seu nome\"\n />\n </div>\n ) : null}\n <div>\n <label htmlFor=\"auth-phone\" className=\"mb-1 block text-sm font-medium text-foreground\">Celular</label>\n <div className=\"flex\">\n <select\n aria-label=\"País\"\n value={countryIso}\n onChange={(e) => { const c = getCountry(e.target.value); setCountryIso(c.iso2); setPhone(maskPhone(unmaskPhone(phone), c.mask)) }}\n 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\"\n >\n {COUNTRIES.map((c) => <option key={c.iso2} value={c.iso2}>{c.flag} {c.dial}</option>)}\n </select>\n <input\n id=\"auth-phone\" type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" value={phone} autoFocus\n onChange={(e) => setPhone(maskPhone(e.target.value, country.mask))}\n 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\"\n placeholder={`Ex.: ${maskPhone('11987654321', country.mask)}`}\n />\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={unmaskPhone(phone).length < 8}\n 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\"\n >\n Enviar código via WhatsApp\n </button>\n <p className=\"text-center text-xs text-muted-foreground\">Enviaremos um código para confirmar seu número.</p>\n </form>\n ) : (\n <form onSubmit={(e) => { e.preventDefault(); void confirmCode() }} className=\"mt-5 space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n Enviamos um código via WhatsApp para <span className=\"font-medium text-foreground\">{country.flag} {country.dial} {phone}</span>.\n </p>\n <input\n type=\"text\" inputMode=\"numeric\" value={code} autoFocus\n onChange={(e) => { setCode(e.target.value); setCodeError(false) }}\n 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\"\n placeholder=\"Digite o código\"\n />\n {codeError ? <p role=\"alert\" className=\"text-sm text-destructive\">Código inválido. Para testar, use 0000.</p> : null}\n <button\n type=\"submit\"\n disabled={busy}\n 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\"\n >\n {busy ? 'Entrando…' : mode === 'signup' ? 'Criar conta e entrar' : 'Confirmar e entrar'}\n </button>\n <button type=\"button\" onClick={() => setStep('phone')} className=\"mx-auto flex items-center gap-1 text-sm font-medium text-primary hover:underline\">\n <ArrowLeft className=\"h-3.5 w-3.5\" /> Trocar número\n </button>\n </form>\n )}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n )\n}\n","import { createContext, useContext, useState, type ReactNode } from 'react'\nimport { PhoneAuthModal, type PhoneAuthModalConfig } from './PhoneAuthModal'\n\ninterface AuthModalContextValue {\n openAuthModal: () => void\n closeAuthModal: () => void\n isAuthModalOpen: boolean\n}\n\nconst AuthModalContext = createContext<AuthModalContextValue | null>(null)\n\nexport interface AuthModalProviderProps {\n /** Enables the phone sign-in/up modal (default for dogfood sites). */\n phoneAuth?: boolean\n config?: PhoneAuthModalConfig\n children: ReactNode\n}\n\n/**\n * Mounts the default phone-auth login modal and exposes openAuthModal() to the\n * host (e.g. an \"Acessar área do cliente\" button). Render INSIDE <AuthProvider>\n * so the modal can sign the user in through the mounted adapter.\n */\nexport function AuthModalProvider({ phoneAuth = true, config, children }: AuthModalProviderProps) {\n const [open, setOpen] = useState(false)\n const value: AuthModalContextValue = {\n openAuthModal: () => setOpen(true),\n closeAuthModal: () => setOpen(false),\n isAuthModalOpen: open,\n }\n return (\n <AuthModalContext.Provider value={value}>\n {children}\n {phoneAuth ? <PhoneAuthModal open={open} onClose={() => setOpen(false)} config={config} /> : null}\n </AuthModalContext.Provider>\n )\n}\n\n/** Access the auth modal controls. Returns a no-op opener if no provider is mounted. */\nexport function useAuthModal(): AuthModalContextValue {\n return (\n useContext(AuthModalContext) ?? {\n openAuthModal: () => {},\n closeAuthModal: () => {},\n isAuthModalOpen: false,\n }\n )\n}\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Canonical phone → auth email. Used by BOTH the phone-auth modal and any host
3
+ * booking→auth bridge so the same number always maps to the same account
4
+ * (Quaddro model: phone = identity). Pass the phone WITH its country dial so the
5
+ * digits are globally unique.
6
+ */
7
+ export declare function phoneToEmail(phoneWithDial: string): string;
8
+ //# sourceMappingURL=phone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phone.d.ts","sourceRoot":"","sources":["../../src/website/phone.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAG1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-auth",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Fayz SDK — reusable auth runtime and UI integration plugin",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -12,6 +12,12 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js",
14
14
  "require": "./dist/index.cjs"
15
+ },
16
+ "./website": {
17
+ "source": "./src/website/index.tsx",
18
+ "types": "./dist/website/index.d.ts",
19
+ "import": "./dist/website/index.js",
20
+ "require": "./dist/website/index.cjs"
15
21
  }
16
22
  },
17
23
  "files": [
@@ -23,9 +29,11 @@
23
29
  "react-dom": "^18.0.0 || ^19.0.0"
24
30
  },
25
31
  "dependencies": {
26
- "@fayz-ai/auth": "^0.6.6",
27
- "@fayz-ai/core": "^0.6.6",
28
- "@fayz-ai/ui": "^0.6.6"
32
+ "@radix-ui/react-dialog": "^1.1.0",
33
+ "lucide-react": "^0.400.0",
34
+ "@fayz-ai/auth": "^0.7.1",
35
+ "@fayz-ai/core": "^0.7.1",
36
+ "@fayz-ai/ui": "^0.7.0"
29
37
  },
30
38
  "devDependencies": {
31
39
  "@types/react": "^18.3.0",
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,178 @@
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
+ }
@@ -0,0 +1,17 @@
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'
@@ -0,0 +1,12 @@
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
+ }