your-ai-workflow-firebase-os 1.13.1 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ npm pack # in this repo
65
65
  npm install /path/to/your-ai-workflow-firebase-os-1.0.0.tgz # in your app
66
66
  ```
67
67
 
68
- Installing runs a postinstall step that patches the consumer project: it clears Vite's default CSS, writes `src/main.tsx` and `src/App.tsx` (both are owned by the library — see below), adds `yourAiFirebase()` from `your-ai-workflow-firebase-os/vite` and Tailwind to `vite.config.ts`, creates `src/your-ai-workflow-firebase-os/` with the editable theme and configs, and writes `GEMINI.md` / `AGENTS.md`.
68
+ Installing runs a postinstall step that patches the consumer project: it clears Vite's default CSS, writes `src/main.tsx` and `src/App.tsx` (both are owned by the library — see below), adds `yourAiFirebase()` from `your-ai-workflow-firebase-os/vite` and Tailwind to `vite.config.ts`, and creates `src/your-ai-workflow-firebase-os/` with the editable theme and configs. It writes no `GEMINI.md` / `AGENTS.md`: AI Studio reserves those filenames for the developer's own instructions, and a package writing them reads as prompt injection.
69
69
 
70
70
  ### What the build enforces
71
71
 
@@ -210,9 +210,9 @@ All CSS is scoped to `.firebase-os` — the library's styles **do not leak** int
210
210
 
211
211
  ## 🤖 Working with the AI
212
212
 
213
- Your AI Firebase is built to be driven from an AI chat (Google AI Studio, Cursor, Claude Code…). On install it writes **`GEMINI.md` / `AGENTS.md`** into the project: what the assistant may build, which words to use with the owner, and what it must never touch.
213
+ Your AI Firebase is built to be driven from an AI chat (Google AI Studio, Cursor, Claude Code…). The contract says what the assistant may build, which words to use with the owner, and what it must never touch.
214
214
 
215
- **That file is the contract, and it is the only copy.** It is generated by [`scripts/postinstall.js`](scripts/postinstall.js) read it there, and change it there. This README deliberately does not restate it, so the two can never disagree.
215
+ **That contract lives in [`src/contract/aiRules.ts`](src/contract/aiRules.ts), and it is the only copy.** Read it there, and change it there. Nothing writes it into the consumer project and no screen hands it over: an assistant that should have it is given it by the owner, in whatever file that assistant reads. This README deliberately does not restate it, so the two can never disagree.
216
216
 
217
217
  The short version for a person evaluating the library: you ask for a page or a form in plain language, the assistant writes it, and it appears on the `/pages` screen where the basics — name, web address, icon, order, visibility — are yours to edit. A form is created with its page there, and once built it also shows up on `/forms`, where you edit its title, questions and button text yourself. Everything else is the assistant's job; ask it in chat.
218
218
 
@@ -254,7 +254,7 @@ src/
254
254
  ├── templates/ ← Template components (internal)
255
255
  ├── components/ ← Shared UI, incl. components/fields (internal)
256
256
  ├── configs/ ← Default configurations (internal)
257
- └── contract/ ← Tests pinning the generated GEMINI.md contract
257
+ └── contract/ ← The assistant's contract, and the tests pinning it
258
258
  ```
259
259
 
260
260
  ### Scripts
@@ -0,0 +1,389 @@
1
+ const e=require(`./chunk-DYteiM-1.cjs`),t=require(`./TimeZoneField-ZNmClPqb.cjs`);let n=require(`react`);n=e.i(n,1);let r=require(`firebase/firestore`),i=require(`firebase/auth`),a=require(`react/jsx-runtime`),o=require(`react-router-dom`);var s=[`free`,`internal`,`admin`],c=[`user`,`internal`,`admin`],l={free:`Free`,internal:`Internal`,admin:`Admin`};function u(e){return e===`admin`?`admin`:e===`internal`?`internal`:`free`}function d(e){return e===`free`?`user`:e}var f=e=>u(e)===`admin`,p=e=>u(e)!==`free`;function m(e){return e===`admin`?`admin`:e===`public`?`public`:e===`internal`||e===`shared`?`internal`:`user`}function h(e){let t=m(e);return t===`public`?`user`:t}function g(e,t){switch(m(t)){case`public`:return!0;case`admin`:return f(e);case`internal`:return p(e);case`user`:return!0}}function _(e,t=!0){if(!t)return`guest`;let n=u(e);return n===`free`?`user`:n}function v(e){switch(m(e)){case`public`:return[`admin`,`internal`,`user`,`guest`];case`admin`:return[`admin`];case`internal`:return[`admin`,`internal`];case`user`:return[`admin`,`internal`,`user`]}}var y=(0,n.createContext)({user:null,userRole:null,accessLevel:`free`,isInternal:!1,isAdmin:!1,loading:!0,signOut:async()=>{},registerUser:async()=>{},refreshUser:async()=>{},userInactive:!1,roleResolved:!1,activeOrg:null,activeWorkspace:null,checkOrg:async()=>{},workspaceAccess:{},userWorkspaces:[],switchWorkspace:async()=>{},createWorkspace:async()=>{},createOrg:async()=>{}});function b({children:e}){let[o,s]=(0,n.useState)(null),[c,l]=(0,n.useState)(null),[d,m]=(0,n.useState)(!1),[h,g]=(0,n.useState)(!0),[_,v]=(0,n.useState)(!1),b=n.default.useRef(null),x=n.default.useRef(null),S=n.default.useRef(new Set),C=e=>(0,r.updateDoc)((0,r.doc)(t.Sr,`user_profiles`,e),{role:`admin`}),w=async e=>{if(!S.current.has(e)){S.current.add(e);try{await C(e)}catch(n){if(n?.code!==`permission-denied`){console.error(`Error claiming the admin role:`,n);return}try{if(!t.xr.currentUser)return;await t.xr.currentUser.getIdToken(!0),await C(e)}catch(e){e?.code!==`permission-denied`&&console.error(`Error claiming the admin role:`,e)}}}};return(0,n.useEffect)(()=>{let e;if(!t.xr){g(!1),v(!0);return}(0,i.getRedirectResult)(t.xr).catch(()=>{});let n=(0,i.onAuthStateChanged)(t.xr,async n=>{if(n?.uid===x.current&&_){s(n);return}if(x.current=n?.uid||null,n){let i=localStorage.getItem(`fbos_role_${n.uid}`);i&&(l(i),v(!0),g(!1)),console.log(`[AuthContext] 1. onAuthStateChanged fired for user:`,n.uid),s(n);try{let i=(0,r.doc)(t.Sr,`user_profiles`,n.uid),a=(await(0,r.getDoc)(i)).data()||{};(0,r.setDoc)(i,{uid:n.uid,email:n.email,displayName:n.displayName||a.displayName||``,lastLoginAt:new Date().toISOString(),createdAt:a.createdAt||new Date().toISOString()},{merge:!0}).catch(e=>console.error(`Failed to sync profile:`,e)),e&&e(),e=(0,r.onSnapshot)(i,e=>{if(e.exists()){let r=e.data(),i=r.role||`user`;!r.role&&t.xr.currentUser?.emailVerified&&w(n.uid),b.current===null&&(b.current=i),l(i),localStorage.setItem(`fbos_role_${n.uid}`,i),m(r.inactive===!0)}v(!0),g(!1)},e=>{console.warn(`[AuthContext] user_profiles snapshot error (safe to ignore during auth transition):`,e),v(!0),g(!1)})}catch(e){console.error(`[AuthContext] ERROR in auth resolution:`,e),v(!0),g(!1)}}else console.log(`[AuthContext] No user currently signed in.`),s(null),l(null),m(!1),v(!0),g(!1),b.current=null,e&&e()});return()=>{n(),e&&e()}},[]),(0,n.useEffect)(()=>{if(!o?.emailVerified||c===`admin`)return;let e=!1;return(0,r.getDoc)((0,r.doc)(t.Sr,`user_profiles`,o.uid)).then(t=>{!e&&t.exists()&&!t.data().role&&w(o.uid)}).catch(()=>{}),()=>{e=!0}},[o?.uid,o?.emailVerified,c]),(0,a.jsx)(y.Provider,{value:{user:o,userRole:c,accessLevel:u(c),isInternal:p(c),isAdmin:f(c),loading:h,signOut:async()=>{await(0,i.signOut)(t.xr),s(null)},registerUser:async(e,n,a,o)=>{let{createUserWithEmailAndPassword:s}=await import(`firebase/auth`),{user:c}=await s(t.xr,e,n),l=`${a} ${o}`.trim();await(0,i.updateProfile)(c,{displayName:l});try{await c.getIdToken(!0),await(0,r.setDoc)((0,r.doc)(t.Sr,`user_profiles`,c.uid),{uid:c.uid,email:c.email,displayName:l,createdAt:new Date().toISOString()},{merge:!0})}catch(e){console.warn(`Could not write public profile on registration.`,e)}await(0,i.sendEmailVerification)(c)},refreshUser:async()=>{t.xr.currentUser&&(await t.xr.currentUser.reload(),s(Object.assign(Object.create(t.xr.currentUser),{photoURL:t.xr.currentUser.photoURL,displayName:t.xr.currentUser.displayName,email:t.xr.currentUser.email,emailVerified:t.xr.currentUser.emailVerified})))},userInactive:d,roleResolved:_},children:e})}var x=()=>(0,n.useContext)(y),S=`Could not reach the server. Check your connection and try again.`,C={goTo(e){window.location.href=e}};function w(){return typeof window<`u`&&window.location?.origin?`${window.location.origin}/api/payments/webhook`:`/api/payments/webhook`}async function T(){try{let e=t.xr?.currentUser;return e?await e.getIdToken():null}catch{return null}}function E(e){return e?{Authorization:`Bearer ${e}`}:{}}async function D(e,t){try{let t=await e.json();if(t&&typeof t.error==`string`&&t.error)return t.error}catch{}return t}async function ee(e){let t=await te(e);return typeof t==`string`?t:(C.goTo(t.url),null)}async function te(e){try{let t=await T(),n=await fetch(`/api/payments/checkout`,{method:`POST`,headers:{"Content-Type":`application/json`,...E(t)},body:JSON.stringify({...e,origin:window.location.origin})});if(!n.ok)return await D(n,`The payment could not be started. Please try again.`);let r=await n.json();return r?.url?{sessionId:String(r.sessionId||``),url:String(r.url)}:`The payment could not be started. Please try again.`}catch{return S}}async function ne(){let e=await T();if(!e)return`Sign in to manage your plan.`;try{let t=await fetch(`/api/payments/portal`,{method:`POST`,headers:{"Content-Type":`application/json`,...E(e)},body:JSON.stringify({origin:window.location.origin})});if(!t.ok)return await D(t,`The billing portal could not be opened.`);let n=await t.json();return n?.url?(C.goTo(String(n.url)),null):`The billing portal could not be opened.`}catch{return S}}async function re(e){let t={paid:!1,paymentStatus:null,amountTotal:null,currency:null,customerEmail:null,metadata:{},error:null};if(!e)return{...t,error:`No payment session to confirm.`};try{let n=await fetch(`/api/payments/session/${encodeURIComponent(e)}`);if(!n.ok)return{...t,error:await D(n,`That payment could not be found.`)};let r=await n.json();return{paid:r?.paid===!0,paymentStatus:r?.paymentStatus??null,amountTotal:r?.amountTotal??null,currency:r?.currency??null,customerEmail:r?.customerEmail??null,metadata:r?.metadata||{},error:null}}catch{return{...t,error:S}}}var O=null;function ie(){return O||=fetch(`/api/payments/status`).then(e=>e.json()).then(e=>({configured:e?.configured===!0&&e?.enabled!==!1,mode:e?.mode??null})).catch(()=>(O=null,{configured:!1,mode:null})),O}function ae(){O=null}function oe(){let[e,t]=(0,n.useState)({loading:!0,configured:!1,mode:null});return(0,n.useEffect)(()=>{let e=!1;return ie().then(n=>{e||t({loading:!1,...n})}),()=>{e=!0}},[]),e}function se(){let[e,t]=(0,n.useState)({loading:!0,active:!1,subscriptions:[],payments:[],error:null});return(0,n.useEffect)(()=>{let e=!1,n=n=>{e||t({loading:!1,...n})};return(async()=>{let e=await T();if(!e)return n({active:!1,subscriptions:[],payments:[],error:null});try{let t=await fetch(`/api/payments/me`,{headers:E(e)});if(!t.ok)return n({active:!1,subscriptions:[],payments:[],error:await D(t,`Your payments could not be loaded.`)});let r=await t.json();return n({active:r?.active===!0,subscriptions:r?.subscriptions||[],payments:r?.payments||[],error:null})}catch{return n({active:!1,subscriptions:[],payments:[],error:S})}})(),()=>{e=!0}},[]),e}function ce(){let[e,t]=(0,n.useState)({loading:!0,charges:[],subscriptions:[],error:null});return(0,n.useEffect)(()=>{let e=!1,n=n=>{e||t({loading:!1,...n})};return(async()=>{let e=await T();if(!e)return n({charges:[],subscriptions:[],error:`Please sign in first.`});try{let t=await fetch(`/api/payments/sales`,{headers:E(e)});if(!t.ok)return n({charges:[],subscriptions:[],error:await D(t,`The sales could not be loaded.`)});let r=await t.json();return n({charges:r?.charges||[],subscriptions:r?.subscriptions||[],error:null})}catch{return n({charges:[],subscriptions:[],error:S})}})(),()=>{e=!0}},[]),e}function le(...e){return t.Dt(t.Ot(e))}var k=.72,A=({className:e,children:t,...r})=>{let i=(0,n.useRef)(null);return(0,n.useLayoutEffect)(()=>{let e=i.current,t=e?.parentElement;if(!e||!t)return;let n=()=>{e.style.removeProperty(`font-size`),e.removeAttribute(`data-fold`);let n=t.clientWidth,r=e.scrollWidth;if(!n||r<=n)return;let i=parseFloat(window.getComputedStyle(e).fontSize)||24,a=n/r;e.style.setProperty(`font-size`,`${(i*Math.max(a,k)).toFixed(2)}px`,`important`),a<k&&e.setAttribute(`data-fold`,``)};if(n(),document.fonts?.ready.then(n).catch(()=>{}),typeof ResizeObserver>`u`)return;let r=new ResizeObserver(n);return r.observe(t),()=>r.disconnect()},[t]),(0,a.jsx)(`h1`,{ref:i,className:le(`page-title w-fit inline-block tracking-tight mb-3 mt-1 text-gradient`,e),...r,children:t})};A.displayName=`PageTitle`;var ue=e=>e.trim().split(`-`).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``),j=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).replace(/([A-Z])([A-Z][a-z])/g,`$1-$2`).toLowerCase(),M=e=>{let t=e.replace(/-/g,` `);return t.charAt(0).toUpperCase()+t.slice(1)},de=(()=>{let e=t.Mt,n=e?Object.keys(e):[];return n.length===0&&(n=Object.keys(t.jt).filter(e=>/^[A-Z]/.test(e)&&!/Icon$/.test(e)&&!/^Lucide/.test(e)&&e!==`IconNode`)),Array.from(new Set(n.map(j))).sort()})(),N=`layers.house.layout-dashboard.list.table.kanban.calendar.clock.file-text.files.folder.hard-drive.upload.download.paperclip.book-open.notebook-pen.users.user.user-plus.contact.building-2.briefcase.handshake.mail.message-square.message-circle.send.inbox.bell.megaphone.phone.shopping-cart.package.truck.receipt.credit-card.banknote.wallet.tag.percent.chart-bar.chart-line.chart-pie.trending-up.target.gauge.activity.settings.sliders-horizontal.wrench.shield.lock.key.eye.search.star.heart.bookmark.flag.pin.map-pin.globe.circle-check.circle-alert.info.circle-help.life-buoy.sparkles.zap.rocket.camera.image.video.music.mic.palette.pen-tool.scissors.stethoscope.graduation-cap.utensils.plane.car.dumbbell.leaf.sun.moon`.split(`.`),P={money:[`banknote`,`wallet`,`credit-card`,`coins`,`dollar-sign`,`receipt`],cash:[`banknote`,`coins`,`wallet`],payment:[`credit-card`,`banknote`,`receipt`],invoice:[`receipt`,`file-text`],price:[`tag`,`dollar-sign`,`receipt`],people:[`users`,`user`,`contact`],customer:[`users`,`contact`,`user-round`],client:[`users`,`contact`,`briefcase`],team:[`users`,`users-round`],staff:[`users`,`id-card`],chat:[`message-circle`,`message-square`,`messages-square`],message:[`message-circle`,`mail`,`send`],email:[`mail`,`send`,`inbox`],task:[`list-checks`,`square-check`,`clipboard-check`],todo:[`list-checks`,`square-check`],project:[`kanban`,`folder-kanban`,`briefcase`],board:[`kanban`,`layout-dashboard`],photo:[`image`,`camera`,`images`],picture:[`image`,`images`],document:[`file-text`,`files`,`file`],doc:[`file-text`,`file`],shop:[`shopping-cart`,`store`,`shopping-bag`],store:[`store`,`shopping-bag`,`shopping-cart`],order:[`shopping-cart`,`package`,`receipt`],delivery:[`truck`,`package`],home:[`house`,`home`],booking:[`calendar-check`,`calendar`,`clock`],appointment:[`calendar-clock`,`calendar`,`clock`],schedule:[`calendar-days`,`calendar`,`clock`],event:[`calendar`,`party-popper`,`ticket`],report:[`chart-bar`,`chart-line`,`file-chart-column`],stats:[`chart-bar`,`chart-line`,`chart-pie`],analytics:[`chart-line`,`chart-bar`,`activity`],help:[`circle-help`,`life-buoy`,`info`],support:[`life-buoy`,`headset`,`circle-help`],idea:[`lightbulb`,`sparkles`],health:[`heart-pulse`,`stethoscope`,`activity`],food:[`utensils`,`chef-hat`,`pizza`],travel:[`plane`,`map`,`luggage`],location:[`map-pin`,`map`,`globe`],address:[`map-pin`,`house`],security:[`shield`,`lock`,`key`],password:[`key`,`lock`],settings:[`settings`,`sliders-horizontal`,`wrench`],file:[`file`,`file-text`,`files`,`folder`],upload:[`upload`,`cloud-upload`],video:[`video`,`film`,`clapperboard`],school:[`graduation-cap`,`book-open`,`school`],course:[`graduation-cap`,`book-open`],gym:[`dumbbell`,`activity`],garden:[`leaf`,`flower`,`sprout`],car:[`car`,`car-front`],pet:[`paw-print`,`dog`,`cat`],note:[`notebook-pen`,`sticky-note`,`pencil`],write:[`pencil`,`pen-tool`,`notebook-pen`],time:[`clock`,`timer`,`hourglass`],star:[`star`,`sparkles`],favourite:[`star`,`heart`,`bookmark`],favorite:[`star`,`heart`,`bookmark`]},fe=(e,t)=>{if(Math.abs(e.length-t.length)>2)return 3;let n=Array.from({length:t.length+1},(e,t)=>t);for(let r=1;r<=e.length;r++){let i=n[0];n[0]=r;for(let a=1;a<=t.length;a++){let o=n[a];n[a]=Math.min(n[a]+1,n[a-1]+1,i+(e[r-1]===t[a-1]?0:1)),i=o}}return n[t.length]},pe=(e,t)=>{let n=0;for(let r of t)r===e[n]&&n++;return n===e.length};function F(e,t=96,n=de){let r=e.trim().toLowerCase().replace(/\s+/g,`-`).replace(/[^a-z0-9-]/g,``);if(!r)return[];let i=r.split(`-`).filter(Boolean),a=new Set(n),o=new Map,s=(e,t)=>{a.has(e)&&o.set(e,Math.max(o.get(e)||0,t))};for(let e of n){let t=e.split(`-`);if(e===r){s(e,100);continue}if(e.startsWith(r)){s(e,90);continue}if(t.some(e=>e===r)){s(e,85);continue}if(t.some(e=>e.startsWith(r))){s(e,75);continue}if(e.includes(r)){s(e,65);continue}if(i.length>1&&i.every(e=>t.some(t=>t.startsWith(e)))){s(e,60);continue}}for(let e of i){for(let t of P[e]||[])s(t,70);if(e.endsWith(`s`))for(let t of P[e.slice(0,-1)]||[])s(t,70)}if(o.size===0&&r.length>=3)for(let e of n)e.split(`-`).some(e=>fe(r,e)<=(r.length>5?2:1))?s(e,40):r.length>=4&&pe(r,e.replace(/-/g,``))&&s(e,20);let c=e=>{let t=N.indexOf(e);return t===-1?N.length:t};return Array.from(o.entries()).sort((e,t)=>t[1]-e[1]||c(e[0])-c(t[0])||e[0].length-t[0].length||e[0].localeCompare(t[0])).slice(0,t).map(([e])=>e)}function me(e){if(!e)return t.En;let n=e.trim(),r=t.jt;return r[ue(n)]||r[n]||t.En}function I({name:e,className:t}){return(0,n.createElement)(me(e),{className:t})}function he({label:e=`Icon`,hint:r=`Shown next to the name in the menu.`,value:i,onChange:o}){let[s,c]=(0,n.useState)(``),l=(i||``).trim(),u=(0,n.useMemo)(()=>F(s),[s]),d=s.trim().length>0;return(0,a.jsx)(t.r,{label:e,hint:r,children:(0,a.jsxs)(`div`,{className:`flex flex-col gap-2`,children:[(0,a.jsxs)(`div`,{className:t.s,children:[l&&!d?(0,a.jsxs)(`span`,{className:`inline-flex items-center gap-2 mr-2.5 shrink-0 min-w-0 text-accent`,children:[(0,a.jsx)(`span`,{className:`w-7 h-7 -ml-1 rounded-lg bg-accent/[0.08] border border-accent/25 flex items-center justify-center shrink-0`,children:(0,a.jsx)(I,{name:l,className:`w-4 h-4`})}),(0,a.jsx)(`span`,{className:`text-[13px] font-semibold truncate`,children:M(j(l))}),(0,a.jsx)(t.nr,{className:`w-3.5 h-3.5 shrink-0`})]}):(0,a.jsx)(t.nn,{className:`w-4 h-4 mr-2.5 text-foreground/30 shrink-0`}),(0,a.jsx)(`input`,{type:`text`,value:s,onChange:e=>c(e.target.value),placeholder:l?`Type a word to change it`:`Type a word, e.g. calendar`,"aria-label":`Search icons`,className:`${t.c} no-glow`})]}),d&&(0,a.jsx)(`div`,{className:`rounded-[var(--input-radius,0.75rem)] border border-[var(--panel-border)] bg-[var(--panel-bg)] p-2 max-h-56 overflow-y-auto`,children:u.length>0?(0,a.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:u.map(e=>{let t=j(l).toLowerCase()===e;return(0,a.jsx)(`button`,{type:`button`,title:M(e),"aria-label":M(e),"aria-pressed":t,onClick:()=>{o(e),c(``)},className:`w-9 h-9 rounded-lg flex items-center justify-center transition-all cursor-pointer ${t?`bg-accent text-[var(--button-fg,#fff)] ring-2 ring-accent/40`:`text-foreground/60 hover:text-foreground hover:bg-foreground/[0.06]`}`,children:(0,a.jsx)(I,{name:e,className:`w-4 h-4`})},e)})}):(0,a.jsx)(`span`,{className:`block px-2 py-2 text-[12px] font-medium text-foreground/45`,children:`Nothing close to that. Try a simpler word, like calendar or people.`})})]})})}var ge=/(<[A-Z][A-Za-z0-9]*(?:\s[^<>]*)?\/?>|src\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]|'[A-Za-z0-9_@./-]+')/g,_e=/(https?:\/\/[^\s]+)/g,L=e=>e.split(_e).flatMap((e,t)=>t%2==1?[{text:e,kind:`value`}]:e.split(ge).map((e,t)=>({text:e,kind:t%2==1?`code`:void 0}))).filter(e=>e.text),ve=/exactly: |Here is (?:what|how) [^:]*: |It should ask for: |Where the form belongs on the page: |a button that says /,ye=e=>{let t=ve.exec(e);if(!t)return L(e);let n=t.index+t[0].length,r=e.slice(0,n),i=e.slice(n),a=i.indexOf(` — `);return[...L(r),{text:a===-1?i:i.slice(0,a),kind:`value`},...L(a===-1?``:i.slice(a))]},be=`text-accent font-semibold bg-accent/[0.08] rounded-[4px] px-1 -mx-[1px] [overflow-wrap:anywhere]`,xe=`font-mono text-[0.92em] text-foreground/75 bg-foreground/[0.055] border border-[var(--panel-border)]/60 rounded-[4px] px-1 py-[1px] [overflow-wrap:anywhere]`;function R({text:e}){return(0,a.jsx)(a.Fragment,{children:ye(e).map((e,t)=>e.kind?(0,a.jsx)(`span`,{className:e.kind===`value`?be:xe,children:e.text},t):(0,a.jsx)(n.default.Fragment,{children:e.text},t))})}function Se({title:e=`Ask your assistant`,description:r,prompt:i,compact:o=!1,locked:s=!1,onCopied:c}){let[l,u]=(0,n.useState)(!1),d=i.includes(`
2
+ `);return s?null:(0,a.jsxs)(`div`,{className:`flex flex-col gap-2 min-w-0 max-w-full rounded-2xl border border-[var(--panel-border)] bg-[var(--panel-bg)] ${o?`px-4 py-3.5`:`px-5 py-4`}`,children:[(0,a.jsxs)(`div`,{className:`flex items-center justify-between gap-3`,children:[(0,a.jsx)(`span`,{className:`text-[12px] font-extrabold tracking-tight text-gradient`,children:e}),(0,a.jsxs)(`button`,{type:`button`,onClick:()=>{navigator.clipboard?.writeText(i),u(!0),setTimeout(()=>u(!1),2e3),c&&setTimeout(c,550)},"aria-label":l?`Copied`:`Copy`,className:`inline-flex items-center justify-center gap-1.5 h-8 w-8 px-0 sm:w-auto sm:px-3 rounded-[var(--btn-radius,0.75rem)] text-[12px] font-semibold shrink-0 border ${l?`copy-btn-done border-emerald-500/30 bg-emerald-500/10 text-emerald-500 cursor-pointer`:`copy-btn cursor-pointer`}`,children:[l?(0,a.jsx)(t.nr,{className:`copy-icon w-3.5 h-3.5`}):(0,a.jsx)(t.Un,{className:`copy-icon w-3.5 h-3.5`}),(0,a.jsx)(`span`,{className:`max-sm:hidden`,children:l?`Copied`:`Copy`})]})]}),r&&(0,a.jsx)(`p`,{className:`text-[12.5px] text-foreground/50 font-medium leading-relaxed break-words`,children:r}),(0,a.jsx)(`div`,{className:`text-[13.5px] font-medium leading-relaxed break-words [overflow-wrap:anywhere]`,children:d?(0,a.jsx)(`div`,{className:`flex flex-col gap-2 text-foreground/80`,children:i.split(`
3
+ `).filter(e=>e.trim()).map((e,t)=>(0,a.jsx)(`p`,{className:`min-w-0`,children:(0,a.jsx)(R,{text:e})},t))}):(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`“`,(0,a.jsx)(R,{text:i}),`”`]})})]})}var z=`w-11 h-11 shrink-0 rounded-[var(--input-radius,0.75rem)] overflow-hidden border border-[var(--panel-border)]`,Ce=e=>{try{return(decodeURIComponent(new URL(e).pathname).split(`/`).pop()||``).replace(/^\d{10,}_/,``)||`Your picture`}catch{return`Your picture`}};function we({label:e,hint:r,value:i,onChange:o,accept:s=`image/*`,action:c,clearable:l=!0,tips:u,onBusyChange:d}){let{user:f}=x(),[p,m]=(0,n.useState)(!1),[h,g]=(0,n.useState)(``),[_,v]=(0,n.useState)(!1),[y,b]=(0,n.useState)(!1),S=(0,a.jsx)(`input`,{type:`file`,accept:s,"aria-label":i?`Replace ${e}`:`Upload ${e}`,disabled:p,onChange:async e=>{let n=e.target.files?.[0];if(e.target.value=``,!(!n||p)){m(!0),d?.(!0),g(``);try{o(await t.wt(n,f))}catch(e){g(t.St(e))}m(!1),d?.(!1),b(!1)}},className:`sr-only`});return(0,a.jsxs)(t.r,{label:e,hint:r,error:h,action:c,children:[i&&!p?(0,a.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,a.jsx)(`div`,{className:`${t.s} min-w-0`,children:(0,a.jsx)(`span`,{className:`flex-1 min-w-0 truncate text-[14px] text-foreground/60 select-none`,children:Ce(i)})}),(0,a.jsxs)(`div`,{className:`${z} relative group/pic`,children:[(0,a.jsx)(`button`,{type:`button`,onClick:()=>b(e=>!e),"aria-label":`Change ${e}`,"aria-expanded":y,className:`block w-full h-full cursor-pointer`,children:(0,a.jsx)(`img`,{src:i,alt:e,className:`w-full h-full object-cover`})}),(0,a.jsxs)(`div`,{className:`absolute inset-0 flex items-center justify-center gap-1 bg-background/85 transition-opacity duration-200 ${y?`opacity-100`:`opacity-0 pointer-events-none group-hover/pic:opacity-100 group-hover/pic:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto`}`,children:[(0,a.jsxs)(`label`,{title:`Replace ${e}`,className:`w-6 h-6 rounded-md flex items-center justify-center text-foreground/60 hover:text-accent hover:bg-foreground/10 transition-colors cursor-pointer`,children:[(0,a.jsx)(t.Gn,{className:`w-3.5 h-3.5`}),S]}),l&&(0,a.jsx)(`button`,{type:`button`,onClick:()=>{g(``),b(!1),o(``)},"aria-label":`Remove ${e}`,title:`Remove ${e}`,className:`w-6 h-6 rounded-md flex items-center justify-center text-foreground/60 hover:text-red-500 hover:bg-red-500/10 transition-colors cursor-pointer`,children:(0,a.jsx)(t.Pt,{className:`w-3.5 h-3.5`})})]})]})]}):(0,a.jsxs)(`label`,{title:`Upload ${e}`,className:`group/up flex items-center gap-2 ${p?`pointer-events-none`:`cursor-pointer`}`,children:[(0,a.jsx)(`span`,{className:`${t.s} select-none text-[14px] ${p?`text-foreground/50`:`text-foreground/35`} group-hover/up:border-accent/40 group-focus-within/up:border-accent`,children:p?`Uploading your picture…`:`Upload a picture`}),(0,a.jsx)(`span`,{"aria-hidden":!0,className:`${z} flex items-center justify-center bg-[var(--panel-bg)] text-foreground/40 group-hover/up:text-accent group-hover/up:border-accent/40 transition-colors`,children:p?(0,a.jsx)(t.yn,{className:`w-4 h-4 animate-spin text-accent`}):(0,a.jsx)(t.Gn,{className:`w-4 h-4`})}),S]}),!!u?.length&&(0,a.jsxs)(`div`,{className:`mt-2 px-1`,children:[(0,a.jsxs)(`button`,{type:`button`,onClick:()=>v(e=>!e),"aria-expanded":_,className:`inline-flex items-center gap-1 text-[11.5px] font-semibold text-foreground/40 hover:text-foreground/70 transition-colors cursor-pointer`,children:[(0,a.jsx)(t.$n,{className:`w-3 h-3 shrink-0 transition-transform duration-200 ${_?`rotate-90`:``}`}),`Which picture should I upload?`]}),_&&(0,a.jsx)(`ul`,{className:`flex flex-col gap-1 mt-1.5 ml-4`,children:u.map(e=>(0,a.jsxs)(`li`,{className:`text-[11.5px] text-foreground/40 font-medium leading-snug flex gap-1.5`,children:[(0,a.jsx)(`span`,{"aria-hidden":!0,className:`text-accent/60`,children:`•`}),e]},e))})]})]})}var Te=[`A wide picture sits best across a page — about 1600 by 900.`,`JPG or PNG. Any size — a big photo is shrunk for you as it uploads, so the page still opens quickly.`,`Your own photo beats a stock one. People can tell.`,`No writing baked into the picture — your assistant sets the words, so they stay readable on a phone.`],Ee=[`A wide picture, not a square one — it sits in a short bar about 40px tall.`,`PNG with a see-through background looks right on both the light and the dark theme.`],De=[`A square picture — 64 by 64 is plenty.`,`One shape or one letter. A whole logo turns to mush at this size.`],Oe=n.default.createContext({}),ke=Object.defineProperty,B=Object.getOwnPropertySymbols,Ae=Object.prototype.hasOwnProperty,je=Object.prototype.propertyIsEnumerable,V=(e,t,n)=>t in e?ke(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,H=(e,t)=>{for(var n in t||={})Ae.call(t,n)&&V(e,n,t[n]);if(B)for(var n of B(t))je.call(t,n)&&V(e,n,t[n]);return e},Me=(0,n.forwardRef)((e,t)=>{let r=H(H({},n.useContext(Oe)),e);return n.createElement(`svg`,H({width:`1.5em`,height:`1.5em`,viewBox:`0 0 24 24`,strokeWidth:1.5,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,color:`currentColor`,ref:t},r),n.createElement(`path`,{d:`M22 12C22 17.5228 17.5228 22 12 22C10.1786 22 8.47087 21.513 7 20.6622L2 21.5L2.83209 16C2.29689 14.7751 2 13.4222 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z`,stroke:`currentColor`,strokeLinecap:`round`,strokeLinejoin:`round`}),n.createElement(`path`,{d:`M12.9604 13.8683L15.0399 13.4624L17 14.2149V16.0385C17 16.6449 16.4783 17.1073 15.8901 16.9783C14.3671 16.6444 11.5997 15.8043 9.67826 13.8683C7.84859 12.0248 7.22267 9.45734 7.01039 8.04128C6.92535 7.47406 7.3737 7 7.94306 7H9.83707L10.572 8.96888L10.1832 11.0701`,stroke:`currentColor`,strokeLinecap:`round`,strokeLinejoin:`round`}))}),Ne=`rules_version = '2';
4
+ service cloud.firestore {
5
+ match /databases/{database}/documents {
6
+
7
+ // ── Who is asking ─────────────────────────────────────────────────────
8
+ function isAuth() { return request.auth != null; }
9
+
10
+ // The profile document is the whole access model: it carries \`role\` and
11
+ // \`inactive\`. Everything below reads it through these two helpers, so the
12
+ // rules engine fetches it once per request no matter how many checks run.
13
+ function profileExists() {
14
+ return exists(/databases/$(database)/documents/user_profiles/$(request.auth.uid));
15
+ }
16
+
17
+ function profile() {
18
+ return get(/databases/$(database)/documents/user_profiles/$(request.auth.uid)).data;
19
+ }
20
+
21
+ // Turning someone off under /users used to be decoration: \`inactive\` was
22
+ // protected from being written (see grantsUntouched) but never read, so the
23
+ // only thing stopping a deactivated person was the Access Denied screen in
24
+ // the app. Anyone who opened devtools and drove the Firebase SDK straight
25
+ // kept every read and write they had before. Now the flag is checked here,
26
+ // where it cannot be walked around.
27
+ // An account with no profile document yet is NOT deactivated — registration
28
+ // seeds the profile a moment after the account exists, and that gap must
29
+ // not lock a brand new person out of their own sign-up.
30
+ function notDeactivated() {
31
+ return isAuth() &&
32
+ (!profileExists() || profile().get('inactive', false) != true);
33
+ }
34
+
35
+ // Firebase flips email_verified once the link in the verification mail is
36
+ // clicked; Google sign-ins arrive already verified. Every rule below
37
+ // spends this rather than isAuth(), so a half-registered account cannot
38
+ // walk past the /verify screen by driving the SDK from a browser console —
39
+ // and, since the deactivation check lives here too, neither can a
40
+ // deactivated one.
41
+ function isVerified() {
42
+ return isAuth() &&
43
+ request.auth.token.email_verified == true &&
44
+ notDeactivated();
45
+ }
46
+
47
+ function role() {
48
+ return profile().role;
49
+ }
50
+
51
+ function hasProfile() {
52
+ return isVerified() && profileExists();
53
+ }
54
+
55
+ function isAdmin() {
56
+ return hasProfile() && role() == 'admin';
57
+ }
58
+
59
+ // Internal members and admins. Registering is NOT enough: an admin has to
60
+ // set the level under /users. Everything a free user must never see —
61
+ // every mem_ collection, which is what internal pages read and write —
62
+ // hangs off this one function.
63
+ function isInternal() {
64
+ return hasProfile() && role() in ['internal', 'admin'];
65
+ }
66
+
67
+ function isDocOwner() {
68
+ return isVerified() &&
69
+ (resource == null || resource.data.uid == request.auth.uid) &&
70
+ (request.resource == null || request.resource.data.uid == request.auth.uid);
71
+ }
72
+
73
+ // ── The first admin ───────────────────────────────────────────────────
74
+ // The Setup page writes your own address in here before you copy the rules,
75
+ // so this list already names you by the time you paste it into Firebase.
76
+ // It is the one and only way a person can hand themselves a role, it can
77
+ // only ever grant 'admin', and it only works from a verified address —
78
+ // otherwise anyone could register under your email and claim the app
79
+ // without ever opening your inbox.
80
+ function adminEmails() {
81
+ return ['your@email.com'];
82
+ }
83
+
84
+ function isSeedingOwnAdmin() {
85
+ return isVerified() &&
86
+ request.auth.token.email.lower() in adminEmails() &&
87
+ request.resource.data.role == 'admin' &&
88
+ request.resource.data.diff(resource.data).affectedKeys().hasOnly(['role']);
89
+ }
90
+
91
+ // \`role\` and \`inactive\` are the entire access model, so they are read-only
92
+ // to the person they describe: admins set them from /users, and the seed
93
+ // above is the single exception. Name, avatar and the rest stay the
94
+ // owner's to write.
95
+ function grantsUntouched() {
96
+ return resource == null
97
+ ? !request.resource.data.keys().hasAny(['role', 'inactive'])
98
+ : !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'inactive']);
99
+ }
100
+
101
+ // user_ collections an ordinary page reads and writes. Profiles and
102
+ // submissions are carved out below — they each have their own rules.
103
+ function isUserData(collectionName) {
104
+ return collectionName.matches('^user_.*') &&
105
+ collectionName != 'user_profiles' &&
106
+ collectionName != 'user_submissions';
107
+ }
108
+
109
+ function ownsUserDoc(docId) {
110
+ return isVerified() && (isDocOwner() || request.auth.uid == docId);
111
+ }
112
+
113
+ // Admins are trusted with everything in this database — that is what the
114
+ // role is. There used to be a per-document owner test bolted onto update
115
+ // and delete for admin_ collections, but the collection rule below grants
116
+ // admins read+write on admin_ outright and rules OR together, so it never
117
+ // denied anything. It has been removed rather than left to read as a
118
+ // protection that is not there. If you ever want one admin's records to be
119
+ // off limits to another, it has to be built in both places at once: here
120
+ // AND in the admin_ collection rule below.
121
+ match /{document=**} {
122
+ allow read, write: if isAdmin();
123
+ }
124
+
125
+ // ── Profiles ──────────────────────────────────────────────────────────
126
+ // Unverified people reach this one document on purpose: registration seeds
127
+ // it before the email goes out, and /verify needs to read it back. What
128
+ // they cannot do is touch role or inactive.
129
+ // Reading stays open to the owner even when deactivated — the app has to
130
+ // be able to see the flag to show the Access Denied screen — but writing
131
+ // does not: a deactivated account changes nothing about itself.
132
+ match /user_profiles/{uid} {
133
+ allow get: if isAuth() && request.auth.uid == uid;
134
+ allow create: if isAuth() && request.auth.uid == uid && grantsUntouched();
135
+ allow update: if isAuth() && request.auth.uid == uid && notDeactivated() &&
136
+ (grantsUntouched() || isSeedingOwnAdmin());
137
+ }
138
+
139
+ match /{collectionName}/{docId} {
140
+
141
+ allow read, write: if collectionName.matches('^admin_.*') && isAdmin();
142
+
143
+ // sys_ holds theme, page and form config. It is read openly because the
144
+ // login screen has to paint itself before anyone has signed in.
145
+ allow read: if collectionName.matches('^sys_.*');
146
+ allow write: if collectionName.matches('^sys_.*') && isAdmin();
147
+
148
+ allow get: if isUserData(collectionName) && ownsUserDoc(docId);
149
+ allow list: if isUserData(collectionName) && isDocOwner();
150
+ allow write: if isUserData(collectionName) && ownsUserDoc(docId);
151
+
152
+ // Public forms post here from pages a guest can open, so creating stays
153
+ // open to everyone. Nothing else is: editing and deleting are the
154
+ // admin's, via the wildcard above. Without that split anyone could
155
+ // quietly overwrite or wipe every submission you have received.
156
+ allow read: if collectionName == 'user_submissions' && (isDocOwner() || isAdmin());
157
+ allow create: if collectionName == 'user_submissions';
158
+
159
+ allow read, create: if collectionName.matches('^mem_.*') && isInternal();
160
+ allow update, delete: if collectionName.matches('^mem_.*') && isInternal() && (isDocOwner() || request.auth.uid == docId);
161
+
162
+ // ── Booked times (the Schedule screen) ────────────────────────────────
163
+ // pub_bookings holds one document per TAKEN time and nothing about the
164
+ // person — the team member, the start, the end. Its id is the time
165
+ // itself, so a second booking of the same slot cannot be created: that
166
+ // is what stops a double booking. Anyone may take a time, guests
167
+ // included, exactly as anyone may send a public form — and the key list
168
+ // keeps a name or an email from ever landing in a public document.
169
+ allow create: if collectionName == 'pub_bookings'
170
+ && request.resource.data.keys().hasOnly(['memberId', 'start', 'end', 'createdAt']);
171
+ // mem_bookings is who booked and what they answered — the team's copy,
172
+ // written in the same breath as the form entry. Anyone may create one;
173
+ // the team reads and cancels (the mem_ lines above and this one); the
174
+ // person who booked, when they were signed in, may read their own.
175
+ allow create: if collectionName == 'mem_bookings'
176
+ && request.resource.data.keys().hasOnly(['memberId', 'memberName', 'scheduleId', 'scheduleName', 'paymentId', 'paymentSession', 'start', 'end', 'timeZone', 'name', 'email', 'answers', 'formId', 'entryId', 'uid', 'createdAt']);
177
+ allow read: if collectionName == 'mem_bookings' && isDocOwner();
178
+ allow update, delete: if collectionName == 'mem_bookings' && isInternal();
179
+
180
+ // Public pages are readable by guests — that is what makes them public —
181
+ // but writing is not anonymous. Left open, anyone on the internet could
182
+ // empty every public collection you have. Guest input goes through
183
+ // user_submissions instead.
184
+ allow read: if collectionName.matches('^pub_.*');
185
+ allow write: if collectionName.matches('^pub_.*') && isVerified();
186
+
187
+ match /{allPaths=**} {
188
+ allow read: if (collectionName.matches('^admin_.*') && isAdmin())
189
+ || collectionName.matches('^sys_.*')
190
+ || collectionName.matches('^pub_.*')
191
+ || (collectionName.matches('^mem_.*') && isInternal())
192
+ || (collectionName.matches('^user_.*') && isDocOwner());
193
+
194
+ allow create: if (collectionName.matches('^admin_.*') && isAdmin())
195
+ || (collectionName.matches('^sys_.*') && isAdmin())
196
+ || (isUserData(collectionName) && isDocOwner())
197
+ || collectionName == 'user_submissions'
198
+ || (collectionName.matches('^pub_.*') && isVerified())
199
+ || (collectionName.matches('^mem_.*') && isInternal());
200
+
201
+ allow update, delete: if collectionName.matches('^pub_.*') && isVerified();
202
+ }
203
+ }
204
+ }
205
+ }
206
+ `,Pe=`rules_version = '2';
207
+ service firebase.storage {
208
+ match /b/{bucket}/o {
209
+
210
+ // --- Helper Functions ---
211
+ function isAuth() {
212
+ return request.auth != null;
213
+ }
214
+
215
+ function hasProfile(uid) {
216
+ return firestore.exists(/databases/(default)/documents/user_profiles/$(uid));
217
+ }
218
+
219
+ function profileRole(uid) {
220
+ return firestore.get(/databases/(default)/documents/user_profiles/$(uid)).data.role;
221
+ }
222
+
223
+ // Matches the Firestore rules: an admin who switches someone off under
224
+ // /users expects the files to close too. Until this was checked, only the
225
+ // app's own Access Denied screen stood in the way, and a deactivated person
226
+ // driving the SDK from a console kept every file they had. An account with
227
+ // no profile document yet counts as active — registration seeds the profile
228
+ // a moment after the account exists.
229
+ function notDeactivated() {
230
+ return isAuth() &&
231
+ (!hasProfile(request.auth.uid) ||
232
+ firestore.get(/databases/(default)/documents/user_profiles/$(request.auth.uid))
233
+ .data.get('inactive', false) != true);
234
+ }
235
+
236
+ // Matches the Firestore rules: a person who never opened the verification
237
+ // email has a signed-in token but no verified one, and nothing here opens
238
+ // for them. Google sign-ins arrive verified.
239
+ function isVerified() {
240
+ return isAuth() && request.auth.token.email_verified == true && notDeactivated();
241
+ }
242
+
243
+ function isMemberDocAdmin(uid) {
244
+ return hasProfile(uid) && profileRole(uid) == 'admin';
245
+ }
246
+
247
+ function isAdmin() {
248
+ return isVerified() && isMemberDocAdmin(request.auth.uid);
249
+ }
250
+
251
+ // Files in mem_ folders belong to internal pages and to Drive's internal
252
+ // folder. Signing up does not open them — an admin marks the account
253
+ // internal under /users first.
254
+ function isInternal() {
255
+ return isVerified() && hasProfile(request.auth.uid) &&
256
+ profileRole(request.auth.uid) in ['internal', 'admin'];
257
+ }
258
+
259
+ function hasOwnerMeta(reqRes) {
260
+ return reqRes != null && 'metadata' in reqRes && reqRes.metadata != null && 'ownerId' in reqRes.metadata;
261
+ }
262
+
263
+ // --- Upload limits ---
264
+ // Nothing capped uploads before this: any verified account could push a
265
+ // file of any size and any kind into user_ or mem_, and the bill is the
266
+ // owner's. 10 MB covers the documents, photos and short clips these pages
267
+ // are for. Admins are exempt on purpose — the Assets screen is where the
268
+ // owner puts their own logo, fonts and anything else the app needs.
269
+ function underSizeCap(bytes) {
270
+ return request.resource != null && request.resource.size < bytes;
271
+ }
272
+
273
+ // An allowlist rather than a blocklist, so a new dangerous type does not
274
+ // arrive open. SVG is excluded from images because it can carry script and
275
+ // Firebase serves it inline from the storage domain; the admin Assets path
276
+ // can still take one.
277
+ function allowedFileType() {
278
+ return request.resource != null && request.resource.contentType != null && (
279
+ (request.resource.contentType.matches('image/.*') &&
280
+ !request.resource.contentType.matches('image/svg.*')) ||
281
+ request.resource.contentType.matches('audio/.*') ||
282
+ request.resource.contentType.matches('video/.*') ||
283
+ request.resource.contentType in [
284
+ 'text/plain', 'text/csv', 'text/markdown', 'text/calendar',
285
+ 'application/pdf', 'application/json', 'application/rtf',
286
+ 'application/zip', 'application/x-zip-compressed',
287
+ 'application/msword',
288
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
289
+ 'application/vnd.ms-excel',
290
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
291
+ 'application/vnd.ms-powerpoint',
292
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
293
+ 'application/vnd.oasis.opendocument.text',
294
+ 'application/vnd.oasis.opendocument.spreadsheet',
295
+ 'application/vnd.oasis.opendocument.presentation'
296
+ ]
297
+ );
298
+ }
299
+
300
+ function isUploadableImage() {
301
+ return request.resource != null &&
302
+ request.resource.contentType != null &&
303
+ request.resource.contentType.matches('image/.*') &&
304
+ !request.resource.contentType.matches('image/svg.*');
305
+ }
306
+
307
+ // --- Special Match: User Avatars ---
308
+ // A picture, and a small one: the profile screen already refuses anything
309
+ // over 2 MB, this is the same rule where it cannot be skipped.
310
+ match /user_avatars/{uid}/{fileName} {
311
+ allow read: if isVerified();
312
+ allow create, update: if isVerified()
313
+ && request.auth.uid == uid
314
+ && isUploadableImage()
315
+ && underSizeCap(5 * 1024 * 1024)
316
+ && (hasOwnerMeta(request.resource) ? request.resource.metadata.ownerId == request.auth.uid : true);
317
+ allow delete: if isVerified() && request.auth.uid == uid;
318
+ }
319
+
320
+ // --- Generic Folder Match ---
321
+ match /{folder}/{allPaths=**} {
322
+
323
+ // Public folder access
324
+ allow read: if folder.matches('^pub_.*');
325
+
326
+ // Internal folder access — internal members and admins only.
327
+ // The folder test comes first on purpose: isInternal() reaches across to
328
+ // Firestore, so every read of a pub_ file would otherwise pay for a
329
+ // profile lookup it does not need. (Signed-in reads pay one either way
330
+ // now — isVerified() has to look the deactivation flag up.)
331
+ allow read: if folder.matches('^mem_.*') && isInternal();
332
+ allow create: if folder.matches('^mem_.*')
333
+ && isInternal()
334
+ && underSizeCap(10 * 1024 * 1024)
335
+ && allowedFileType()
336
+ && hasOwnerMeta(request.resource)
337
+ && request.resource.metadata.ownerId == request.auth.uid;
338
+ allow update: if folder.matches('^mem_.*')
339
+ && isInternal()
340
+ && underSizeCap(10 * 1024 * 1024)
341
+ && allowedFileType()
342
+ && resource != null
343
+ && hasOwnerMeta(resource)
344
+ && resource.metadata.ownerId == request.auth.uid;
345
+ allow delete: if folder.matches('^mem_.*')
346
+ && isInternal()
347
+ && resource != null
348
+ && hasOwnerMeta(resource)
349
+ && resource.metadata.ownerId == request.auth.uid;
350
+
351
+ // Form attachments. A visitor answering a public form is not signed in,
352
+ // so this one folder takes an anonymous create — capped and
353
+ // type-filtered like every other non-admin write, and write-once: no
354
+ // update, no delete, so a submitted file cannot be swapped after the
355
+ // owner has read the entry. The pub_ prefix is what makes the file
356
+ // readable back, which the download link in the Inbox entry needs.
357
+ allow create: if folder == 'pub_form_uploads'
358
+ && underSizeCap(10 * 1024 * 1024)
359
+ && allowedFileType();
360
+
361
+ // User-specific folders
362
+ allow create: if isVerified()
363
+ && folder.matches('^user_.*')
364
+ && underSizeCap(10 * 1024 * 1024)
365
+ && allowedFileType()
366
+ && hasOwnerMeta(request.resource)
367
+ && request.resource.metadata.ownerId == request.auth.uid;
368
+ allow update: if isVerified()
369
+ && folder.matches('^user_.*')
370
+ && underSizeCap(10 * 1024 * 1024)
371
+ && allowedFileType()
372
+ && resource != null
373
+ && hasOwnerMeta(resource)
374
+ && resource.metadata.ownerId == request.auth.uid;
375
+ allow read, delete: if isVerified()
376
+ && folder.matches('^user_.*')
377
+ && resource != null
378
+ && hasOwnerMeta(resource)
379
+ && resource.metadata.ownerId == request.auth.uid;
380
+
381
+ // Admin access overrides — the Assets screen holds the logo, the favicon
382
+ // and anything else the owner needs the app to serve, so it is not capped
383
+ // or filtered.
384
+ allow read, create: if isAdmin();
385
+ allow update, delete: if isAdmin();
386
+ }
387
+ }
388
+ }
389
+ `,Fe=n.default.lazy(()=>Promise.resolve().then(()=>require(`./SetupPayments-C-zpS99n.cjs`)).then(e=>({default:e.StripeSetupSteps}))),Ie=n.default.lazy(()=>Promise.resolve().then(()=>require(`./SetupPayments-C-zpS99n.cjs`)).then(e=>({default:e.EmailSetupSteps}))),U=[{key:`VITE_FIREBASE_API_KEY`,label:`API Key`,shownAs:`apiKey`},{key:`VITE_FIREBASE_AUTH_DOMAIN`,label:`Auth Domain`,shownAs:`authDomain`},{key:`VITE_FIREBASE_PROJECT_ID`,label:`Project ID`,shownAs:`projectId`},{key:`VITE_FIREBASE_STORAGE_BUCKET`,label:`Storage Bucket`,shownAs:`storageBucket`},{key:`VITE_FIREBASE_MESSAGING_SENDER_ID`,label:`Messaging Sender ID`,shownAs:`messagingSenderId`},{key:`VITE_FIREBASE_APP_ID`,label:`App ID`,shownAs:`appId`}];function Le(e){let t;try{if(t={BASE_URL:`/`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1,VITE_ADMIN_EMAILS:void 0,VITE_FIREBASE_API_KEY:void 0,VITE_FIREBASE_APP_ID:void 0,VITE_FIREBASE_AUTH_DOMAIN:void 0,VITE_FIREBASE_MESSAGING_SENDER_ID:void 0,VITE_FIREBASE_PROJECT_ID:void 0,VITE_FIREBASE_STORAGE_BUCKET:void 0}?.[e],t&&typeof t==`string`&&t.trim()!==``)return t}catch{}try{if(t=(typeof window<`u`?window:globalThis)?.process?.env?.[e],t&&typeof t==`string`&&t.trim()!==``)return t}catch{}try{switch(e){case`VITE_FIREBASE_API_KEY`:t=void 0;break;case`VITE_FIREBASE_AUTH_DOMAIN`:t=void 0;break;case`VITE_FIREBASE_PROJECT_ID`:t=void 0;break;case`VITE_FIREBASE_STORAGE_BUCKET`:t=void 0;break;case`VITE_FIREBASE_MESSAGING_SENDER_ID`:t=void 0;break;case`VITE_FIREBASE_APP_ID`:t=void 0;break;case`VITE_ADMIN_EMAILS`:t=void 0;break}if(t&&typeof t==`string`&&t.trim()!==``)return t}catch{}return``}function W(e,t){let n=Le(e);if(n)return n;if(t?.firebaseConfig){let n={VITE_FIREBASE_API_KEY:`apiKey`,VITE_FIREBASE_AUTH_DOMAIN:`authDomain`,VITE_FIREBASE_PROJECT_ID:`projectId`,VITE_FIREBASE_STORAGE_BUCKET:`storageBucket`,VITE_FIREBASE_MESSAGING_SENDER_ID:`messagingSenderId`,VITE_FIREBASE_APP_ID:`appId`}[e];if(n&&t.firebaseConfig[n])return t.firebaseConfig[n]}return e===`VITE_ADMIN_EMAILS`&&t?.adminEmails?Array.isArray(t.adminEmails)?t.adminEmails.join(`,`):t.adminEmails||``:``}function Re(e){let t=[],n=[],r=t=>W(t,e);for(let{key:e,label:i}of U){let a=r(e);a?a.trim()===``&&n.push(i):t.push(i)}let i=[...t,...n];return{allSet:i.length===0,missing:i,empty:n}}function ze(){return Re().allSet}var Be=Pe;function Ve(e){let t=e.map(e=>e.trim().toLowerCase()).filter(Boolean).filter((e,t,n)=>n.indexOf(e)===t),n=t.length?t.map(e=>`'${e.replace(/'/g,``)}'`).join(`, `):`'your@email.com'`;return Ne.replace(/(function adminEmails\(\) \{\s*return )\[[^\]]*\];/,`$1[${n}];`)}function G(e){return e.split(/[,\s]+/).map(e=>e.trim()).filter(Boolean)}var He=/^[^@\s]+@[^@\s]+\.[^@\s]+$/;function K({code:e,label:r,compact:i}){let[o,s]=n.default.useState(!1);return(0,a.jsxs)(t.kt.button,{whileHover:{scale:1.02},whileTap:{scale:.98},onClick:()=>{navigator.clipboard?.writeText(e),s(!0),setTimeout(()=>s(!1),2e3)},className:`inline-flex items-center font-bold transition-all shrink-0 cursor-pointer ${i?`gap-1.5 h-9 px-3 rounded-xl text-[12px] self-start sm:self-auto`:`gap-2 px-4 py-2 mt-2 rounded-xl text-[13px] tracking-wide`} ${o?`bg-emerald-500/10 text-emerald-500 border border-emerald-500/20`:`btn-secondary${i?``:` shadow-sm`}`}`,children:[o?(0,a.jsx)(t.nr,{className:i?`w-3.5 h-3.5`:`w-4 h-4`}):(0,a.jsx)(t.Un,{className:i?`w-3.5 h-3.5`:`w-4 h-4`}),o?i?`Copied`:`Copied to Clipboard!`:r]})}function q({name:e,shownAs:r,hint:i}){let[o,s]=n.default.useState(!1);return(0,a.jsxs)(`div`,{className:`flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3 px-4 py-3 border-b border-[var(--panel-border)]/40 last:border-0`,children:[(0,a.jsxs)(`div`,{className:`min-w-0`,children:[(0,a.jsx)(`div`,{className:`text-[13.5px] font-bold text-foreground break-all sm:break-normal sm:truncate`,children:e}),(0,a.jsx)(`div`,{className:`text-[12.5px] text-foreground/50 font-medium`,children:i??(0,a.jsxs)(a.Fragment,{children:[`the value Firebase labels `,r]})})]}),(0,a.jsxs)(`button`,{type:`button`,onClick:()=>{navigator.clipboard?.writeText(e),s(!0),setTimeout(()=>s(!1),2e3)},className:`inline-flex items-center gap-1.5 h-9 px-3 rounded-xl text-[12px] font-bold shrink-0 self-start sm:self-auto transition-all cursor-pointer active:scale-[0.98] ${o?`bg-emerald-500/10 text-emerald-500 border border-emerald-500/20`:`btn-secondary`}`,children:[o?(0,a.jsx)(t.nr,{className:`w-3.5 h-3.5`}):(0,a.jsx)(t.Un,{className:`w-3.5 h-3.5`}),o?`Copied`:`Copy name`]})]})}function Ue({initialEmails:e}){let[r,i]=(0,n.useState)(e),[o,s]=(0,n.useState)(``),[c,l]=(0,n.useState)([]),[u,d]=(0,n.useState)(``),f=e=>{i(e),o&&(s(``),l([])),u&&d(``)},p=()=>{let e=G(r);if(e.length===0){d(`Type an email address first — it is what the rules will name.`);return}let t=e.filter(e=>!He.test(e));if(t.length>0){d(`That does not look like an email address: ${t.join(`, `)}`);return}d(``),l(e.map(e=>e.trim().toLowerCase())),s(Ve(e))};return(0,a.jsxs)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl p-5 mb-2 flex flex-col gap-4`,children:[(0,a.jsxs)(`div`,{className:`flex items-center gap-3.5 min-w-0`,children:[(0,a.jsx)(`div`,{className:`w-10 h-10 rounded-2xl flex items-center justify-center shrink-0 border border-[var(--panel-border)]/50 bg-accent/10 text-accent`,children:(0,a.jsx)(t.$t,{className:`w-5 h-5`})}),(0,a.jsxs)(`div`,{className:`flex flex-col gap-0.5 min-w-0`,children:[(0,a.jsx)(`h3`,{className:`text-[15px] font-bold text-foreground tracking-tight leading-tight truncate`,children:`Who runs this app?`}),(0,a.jsx)(`p`,{className:`text-[12px] text-foreground/50 font-normal leading-snug truncate`,children:`The email you sign in with becomes the owner — separate several with commas.`})]})]}),(0,a.jsxs)(`div`,{className:`flex flex-col sm:flex-row sm:items-center gap-2`,children:[(0,a.jsx)(`input`,{type:`text`,value:r,"aria-label":`The email address you will sign in with`,placeholder:`you@yourcompany.com`,onChange:e=>f(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),p())},className:`w-full h-10 px-3.5 rounded-[var(--input-radius,0.75rem)] border border-[var(--panel-border)] bg-[var(--panel-bg)] text-[13.5px] text-foreground outline-none transition-all duration-300 placeholder:text-foreground/35 hover:border-accent/40 focus:border-accent console-field ${u?`is-invalid border-red-500/60`:``}`}),(0,a.jsx)(`button`,{type:`button`,onClick:p,className:`btn-primary inline-flex items-center justify-center h-10 px-4 rounded-[var(--btn-radius,0.75rem)] text-[12.5px] font-bold text-white shrink-0 cursor-pointer active:scale-[0.98] transition-transform whitespace-nowrap`,children:`Generate the rules`})]}),u&&(0,a.jsx)(`p`,{className:`text-[12px] font-semibold text-red-500 leading-snug px-1`,children:u}),o&&(0,a.jsxs)(`div`,{className:`rounded-2xl border border-emerald-500/20 bg-emerald-500/[0.05] px-3.5 py-3 flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3`,children:[(0,a.jsx)(`span`,{className:`w-7 h-7 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0`,children:(0,a.jsx)(t.nr,{className:`w-3.5 h-3.5 text-emerald-500`})}),(0,a.jsxs)(`div`,{className:`min-w-0 flex-1 flex flex-col gap-0.5`,children:[(0,a.jsxs)(`p`,{"data-admin-emails":!0,className:`text-[13px] font-bold text-foreground leading-snug break-all`,children:[`Ready — they make `,c.join(`, `),` the owner.`]}),(0,a.jsx)(`p`,{className:`text-[12px] font-medium text-foreground/50 leading-snug`,children:`Paste them into Firebase, then sign in with that address — or reload, if you already are.`})]}),(0,a.jsx)(K,{compact:!0,code:o,label:`Copy the rules`})]})]})}function J({number:e,title:t,aside:n,children:r}){return(0,a.jsxs)(`div`,{className:`flex flex-col relative pb-10 mb-10 border-b border-[var(--panel-border)]/50 last:border-0 last:pb-0 last:mb-0`,children:[(0,a.jsxs)(`div`,{className:`flex items-center gap-3 mb-4`,children:[(0,a.jsx)(`span`,{className:`w-8 h-8 shrink-0 rounded-full bg-accent/10 border border-accent/20 text-accent flex items-center justify-center text-[14px] font-extrabold`,children:e}),(0,a.jsx)(`h3`,{className:`text-[17px] md:text-[19px] font-extrabold text-foreground tracking-tight flex-1 min-w-0`,children:t}),n]}),(0,a.jsx)(`div`,{className:`text-[14px] md:text-[15px] text-foreground/70 leading-relaxed space-y-4`,children:r})]})}function We({standalone:e}){let r=t.br(),[i]=(0,n.useState)(()=>{let e=localStorage.getItem(`theme-mode`);return e===`light`?!1:e===`dark`?!0:window.matchMedia(`(prefers-color-scheme: dark)`).matches}),o=G(W(`VITE_ADMIN_EMAILS`,r)).join(`, `),[s,c]=(0,n.useState)(()=>{try{return{payments:!1,email:!1,...JSON.parse(localStorage.getItem(`fbos_setup_extras`)||`{}`)}}catch{return{payments:!1,email:!1}}}),l=(e,t)=>{let n={...s,[e]:t};c(n);try{localStorage.setItem(`fbos_setup_extras`,JSON.stringify(n))}catch{}};return(0,n.useEffect)(()=>{e&&document.documentElement.setAttribute(`data-theme`,i?`dark`:`light`)},[i,e]),(0,n.useEffect)(()=>{if(e&&t.Tr.faviconUrl){let e=document.querySelector(`link[rel*='icon']`);e||(e=document.createElement(`link`),e.rel=`icon`,document.head.appendChild(e)),e.href=t.Tr.faviconUrl}},[e]),(0,a.jsxs)(`main`,{className:`flex-1 w-full max-w-7xl mx-auto p-4 pt-2 md:p-8 md:pt-4 z-10 flex flex-col min-h-screen`,children:[(0,a.jsx)(t.kt.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},className:`mb-6 flex items-center justify-between`,children:(0,a.jsxs)(`div`,{children:[(0,a.jsx)(A,{children:`Setup`}),(0,a.jsxs)(`div`,{className:`flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]`,children:[(0,a.jsx)(`span`,{className:`w-8 h-[1px] bg-foreground/10`}),`/setup`]})]})}),(0,a.jsx)(`div`,{className:`flex flex-col gap-6 flex-1 pb-16`,children:(0,a.jsxs)(t.kt.div,{initial:{opacity:0,y:20},animate:{opacity:1,y:0},transition:{delay:.1},className:`glass-panel border border-[var(--panel-border)] rounded-3xl p-4 sm:p-6 md:p-10 shadow-2xl relative overflow-hidden bg-background flex flex-col`,children:[(0,a.jsx)(`div`,{className:`mb-6 pb-4 sm:mb-8 sm:pb-6 border-b border-[var(--panel-border)]/50 relative z-10`,children:(0,a.jsxs)(`div`,{className:`flex max-sm:flex-col max-sm:items-stretch items-center justify-between gap-3 sm:gap-4`,children:[(0,a.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,a.jsx)(`h2`,{className:`text-[17px] sm:text-xl font-extrabold text-foreground tracking-tight truncate`,children:`Connect your app to Firebase`}),(0,a.jsxs)(`p`,{className:`text-[13px] font-medium text-foreground/50 mt-1 sm:mt-1.5 leading-snug truncate`,children:[(0,a.jsx)(`span`,{className:`sm:hidden`,children:`Google's free data and sign-in service.`}),(0,a.jsx)(`span`,{className:`max-sm:hidden`,children:`Firebase is Google's free service that stores your data and signs people in — about ten minutes, once.`})]})]}),(0,a.jsxs)(`div`,{className:`flex items-center gap-2 shrink-0`,children:[(0,a.jsx)(t.kt.a,{whileHover:{scale:1.05},whileTap:{scale:.95},href:`mailto:antonina@youraiworkflow.co`,title:`Email Support`,className:`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer btn-secondary`,children:(0,a.jsx)(t.hn,{className:`w-4 h-4`})}),(0,a.jsx)(t.kt.a,{whileHover:{scale:1.05},whileTap:{scale:.95},href:`https://wa.me/34632407883`,target:`_blank`,rel:`noopener noreferrer`,title:`WhatsApp Support`,className:`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer btn-secondary`,children:(0,a.jsx)(Me,{className:`w-4 h-4 text-foreground/80`})})]})]})}),(0,a.jsxs)(`div`,{className:`relative z-10 p-4 sm:p-5 rounded-3xl border border-[var(--panel-border)]/50 bg-foreground/[0.015] flex flex-col gap-4`,children:[(0,a.jsxs)(`div`,{children:[(0,a.jsx)(`h3`,{className:`text-[14px] font-extrabold text-foreground tracking-tight`,children:`What should your app be able to do?`}),(0,a.jsx)(`p`,{className:`text-[12.5px] font-medium text-foreground/50 mt-0.5 leading-snug`,children:`Switch on what you want and its steps appear at the end of this page. Both can wait — come back and switch them on whenever you like.`})]}),(0,a.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[(0,a.jsxs)(`div`,{className:`flex items-center justify-between gap-4`,children:[(0,a.jsxs)(`div`,{className:`flex flex-col gap-0.5 min-w-0`,children:[(0,a.jsx)(`span`,{className:`text-[13px] font-semibold text-foreground/80`,children:`Take card payments`}),(0,a.jsx)(`span`,{className:`text-[11.5px] text-foreground/40 font-medium leading-snug max-sm:hidden`,children:`Stripe — about five minutes, with pretend money until you say otherwise.`})]}),(0,a.jsx)(t.g,{enabled:s.payments,onChange:e=>l(`payments`,e),label:`Take card payments`})]}),(0,a.jsxs)(`div`,{className:`flex items-center justify-between gap-4`,children:[(0,a.jsxs)(`div`,{className:`flex flex-col gap-0.5 min-w-0`,children:[(0,a.jsx)(`span`,{className:`text-[13px] font-semibold text-foreground/80`,children:`Send email`}),(0,a.jsx)(`span`,{className:`text-[11.5px] text-foreground/40 font-medium leading-snug max-sm:hidden`,children:`Resend — receipts, alerts and confirmations, from one pasted key.`})]}),(0,a.jsx)(t.g,{enabled:s.email,onChange:e=>l(`email`,e),label:`Send email`})]})]})]}),(0,a.jsxs)(`div`,{className:`flex flex-col relative z-10 p-4 sm:p-6 md:p-8 bg-foreground/[0.015] rounded-3xl border border-[var(--panel-border)]/50 mt-4`,children:[(0,a.jsx)(J,{number:1,title:`Create a Firebase project`,children:(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`Open`,` `,(0,a.jsx)(`a`,{href:`https://console.firebase.google.com`,target:`_blank`,rel:`noopener noreferrer`,className:`text-accent underline underline-offset-2 font-semibold hover:text-accent-deep transition-colors`,children:`console.firebase.google.com`}),` `,`and click `,(0,a.jsx)(`strong`,{children:`Add project`}),`. Sign in with your Google account if it asks.`]}),(0,a.jsxs)(`li`,{children:[`Type any name you like and click `,(0,a.jsx)(`strong`,{children:`Continue`}),`.`]}),(0,a.jsxs)(`li`,{children:[`If it offers `,(0,a.jsx)(`strong`,{children:`Enable Gemini in Firebase`}),`, switch it off.`]}),(0,a.jsxs)(`li`,{children:[`Switch off `,(0,a.jsx)(`strong`,{children:`Enable Google Analytics for this project`}),`.`]}),(0,a.jsxs)(`li`,{children:[`Click `,(0,a.jsx)(`strong`,{children:`Create project`}),`, wait, then click `,(0,a.jsx)(`strong`,{children:`Continue`}),`.`]})]})}),(0,a.jsx)(J,{number:2,title:`Add a web app and copy the six values`,children:(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`On that page click `,(0,a.jsx)(`strong`,{children:`+ Add app`}),` under your project's name, then pick the `,(0,a.jsx)(`strong`,{children:`</> Web`}),` icon.`]}),(0,a.jsxs)(`li`,{children:[`Give it any nickname (`,(0,a.jsx)(`span`,{className:`text-accent`,children:`MyApp`}),` is fine). Leave `,(0,a.jsx)(`strong`,{children:`Firebase Hosting`}),` unticked.`]}),(0,a.jsxs)(`li`,{children:[`Click `,(0,a.jsx)(`strong`,{children:`Register app`}),`.`]}),(0,a.jsx)(`li`,{children:`Firebase now shows six values on screen, each with a label beside it — apiKey, authDomain, projectId, storageBucket, messagingSenderId and appId. You will copy them one at a time in the next step, so keep this tab open.`})]})}),(0,a.jsxs)(J,{number:3,title:`Paste the six values into Secrets`,children:[(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`In Google AI Studio, open the `,(0,a.jsx)(`strong`,{children:`Secrets`}),` tab beside your app. For each row below: click `,(0,a.jsx)(`strong`,{children:`Add secret`}),`, copy the name here, paste it as the name, then paste the matching value from your Firebase tab. The names have to match letter for letter — use the Copy name button and don't retype them.`]}),(0,a.jsx)(`p`,{className:`text-foreground/80`,children:`Six rows, and that is all of them. Your own email address is not one of them — you type that straight into step 5 in a moment.`}),(0,a.jsx)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:U.map(({key:e,shownAs:t})=>(0,a.jsx)(q,{name:e,shownAs:t},e))})]}),(0,a.jsx)(J,{number:4,title:`Turn on sign-in`,children:(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`In the Firebase menu on the left, open `,(0,a.jsx)(`strong`,{children:`Authentication`}),` and click `,(0,a.jsx)(`strong`,{children:`Get started`}),`.`]}),(0,a.jsxs)(`li`,{children:[`Open the `,(0,a.jsx)(`strong`,{children:`Sign-in method`}),` tab.`]}),(0,a.jsxs)(`li`,{children:[`Click `,(0,a.jsx)(`strong`,{children:`Email/Password`}),`, switch on the first toggle and `,(0,a.jsx)(`strong`,{children:`Save`}),`. (Leave email link off.)`]}),(0,a.jsxs)(`li`,{children:[`If you also want the `,(0,a.jsx)(`strong`,{children:`Sign in with Google`}),` button to work, click `,(0,a.jsx)(`strong`,{children:`Google`}),`, switch it on, pick your own address as the support email, and `,(0,a.jsx)(`strong`,{children:`Save`}),`.`]}),(0,a.jsxs)(`li`,{children:[`Open the `,(0,a.jsx)(`strong`,{children:`Settings`}),` tab, then `,(0,a.jsx)(`strong`,{children:`Authorized domains`}),`. Your app has to be listed here or nobody can sign in.`]}),(0,a.jsxs)(`li`,{children:[(0,a.jsx)(`strong`,{children:`To get your app's address:`}),` in AI Studio, right-click any link in your app's own menu and choose `,(0,a.jsx)(`strong`,{children:`Copy link address`}),`. Back in Firebase click`,` `,(0,a.jsx)(`strong`,{children:`Add domain`}),`, paste it, and save. It will look something like ais-dev-…run.app — that is the right thing.`]})]})}),(0,a.jsxs)(J,{number:5,title:`Create the database and publish its rules`,children:[(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80 mb-4`,children:[(0,a.jsxs)(`li`,{children:[`In the Firebase menu, open `,(0,a.jsx)(`strong`,{children:`Firestore Database`}),` and click `,(0,a.jsx)(`strong`,{children:`Create database`}),`.`]}),(0,a.jsxs)(`li`,{children:[`Pick `,(0,a.jsx)(`strong`,{children:`Standard edition`}),` if asked, choose the location closest to your people, start in `,(0,a.jsx)(`strong`,{children:`Production mode`}),`, and click Continue.`]}),(0,a.jsxs)(`li`,{children:[`In the box below, type the email address you will sign in with and click `,(0,a.jsx)(`strong`,{children:`Generate the rules`}),`.`]}),(0,a.jsxs)(`li`,{children:[`Click `,(0,a.jsx)(`strong`,{children:`Copy the rules`}),`, then in Firebase open the `,(0,a.jsx)(`strong`,{children:`Rules`}),` tab, select everything that is there, paste over it, and click `,(0,a.jsx)(`strong`,{children:`Publish`}),`.`]})]}),(0,a.jsx)(Ue,{initialEmails:o})]}),(0,a.jsxs)(J,{number:6,title:`Create storage and publish its rules`,children:[(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80 mb-4`,children:[(0,a.jsxs)(`li`,{children:[`In the Firebase menu, open `,(0,a.jsx)(`strong`,{children:`Storage`}),`. Google asks you to upgrade the project first: click `,(0,a.jsx)(`strong`,{children:`Upgrade project`}),` and pick the pay-as-you-go`,` `,(0,a.jsx)(`strong`,{children:`Blaze plan`}),`. Profile photos and attachments cost about $0.03 per gigabyte a month, so this is cents. Set a budget you are comfortable with (say $20), link a billing account, and continue.`]}),(0,a.jsxs)(`li`,{children:[`Click `,(0,a.jsx)(`strong`,{children:`Get started`}),`, choose a no-cost location, start in `,(0,a.jsx)(`strong`,{children:`Production mode`}),`, and click `,(0,a.jsx)(`strong`,{children:`Create`}),`.`]}),(0,a.jsx)(`li`,{children:`Once it exists, Firebase shows the storage address at the top (something like your-project.firebasestorage.app). If it is not exactly what you pasted into the storage secret in step 3, open Secrets and correct that one now.`}),(0,a.jsxs)(`li`,{children:[`Open the `,(0,a.jsx)(`strong`,{children:`Rules`}),` tab, select everything that is there, paste these over it, and click `,(0,a.jsx)(`strong`,{children:`Publish`}),`.`]})]}),(0,a.jsx)(K,{code:Be,label:`Copy the storage rules`})]})]}),s.payments&&(0,a.jsxs)(`div`,{className:`mt-8 relative z-10`,children:[(0,a.jsx)(`h3`,{className:`text-[15px] font-extrabold text-foreground tracking-tight`,children:`Payments — connect Stripe`}),(0,a.jsx)(`p`,{className:`text-[12.5px] font-medium text-foreground/50 mt-0.5 leading-snug`,children:`Stripe is the service that takes cards for you. Test mode means pretend money until the last step — which is yours to take whenever you are ready.`}),(0,a.jsx)(`div`,{className:`flex flex-col p-4 sm:p-6 md:p-8 bg-foreground/[0.015] rounded-3xl border border-[var(--panel-border)]/50 mt-3`,children:(0,a.jsx)(n.default.Suspense,{fallback:null,children:(0,a.jsx)(Fe,{})})})]}),s.email&&(0,a.jsxs)(`div`,{className:`mt-8 relative z-10`,children:[(0,a.jsx)(`h3`,{className:`text-[15px] font-extrabold text-foreground tracking-tight`,children:`Email — connect Resend`}),(0,a.jsx)(`p`,{className:`text-[12.5px] font-medium text-foreground/50 mt-0.5 leading-snug`,children:`One pasted key and your app can send email on its own — receipts, alerts, confirmations.`}),(0,a.jsx)(`div`,{className:`flex flex-col p-4 sm:p-6 md:p-8 bg-foreground/[0.015] rounded-3xl border border-[var(--panel-border)]/50 mt-3`,children:(0,a.jsx)(n.default.Suspense,{fallback:null,children:(0,a.jsx)(Ie,{number:s.payments?6:1})})})]})]})})]})}function Y({trailing:e,onSent:r}){let[i,o]=(0,n.useState)(`idle`),[s,c]=(0,n.useState)(``);return(0,a.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,a.jsxs)(`div`,{className:`flex items-center gap-2.5 flex-wrap`,children:[(0,a.jsx)(t.i,{compact:!0,icon:t.ur,busy:i===`sending`,onClick:async()=>{o(`sending`),c(``);try{let e=await fetch(`/api/_selftest/email`),t=await e.json().catch(()=>({}));e.ok&&t?.ok?(o(`sent`),r?.(typeof t.from==`string`&&t.from.trim()?t.from:null)):(o(`failed`),c(String(t?.error||t?.message||`The server answered ${e.status}.`)))}catch{o(`failed`),c(`The server could not be reached.`)}},title:`Sends the test email to your own admin address`,children:`Send yourself a test email`}),e]}),i===`sent`&&(0,a.jsx)(`p`,{className:`text-[12px] font-semibold text-emerald-500 px-1`,children:`Sent. If you do not receive it within a minute, check your spam.`}),i===`failed`&&(0,a.jsxs)(`p`,{className:`text-[12px] font-medium text-red-500 px-1 leading-relaxed`,children:[`It did not send: `,s,` If the sending address uses your own domain, that domain must be verified under `,(0,a.jsx)(`strong`,{children:`Domains`}),` in your Resend account — an unverified domain makes every send fail.`]})]})}var Ge={checked:!1,configured:!1,mode:null,webhookConfigured:!1};function X(e=5e3){let[t,r]=(0,n.useState)(Ge);return(0,n.useEffect)(()=>{let t=!0,n=async()=>{try{let e=await(await fetch(`/api/payments/status`)).json();if(!t)return;r({checked:!0,configured:!!e?.configured,mode:e?.mode===`live`||e?.mode===`test`?e.mode:null,webhookConfigured:!!e?.webhookConfigured})}catch{t&&r(e=>({...e,checked:!0}))}};n();let i=setInterval(n,e);return()=>{t=!1,clearInterval(i)}},[e]),t}function Z(e,t){if(!t.checked)return{state:`saving`,label:`Checking`};switch(e){case 1:case 2:return t.configured?{state:`saved`,label:`Done`}:{state:`off`,label:`Not yet`};case 3:return t.configured?{state:`saved`,label:`Key found`}:{state:`off`,label:`No key yet`};case 4:return t.configured?{state:`saved`,label:t.mode===`live`?`Connected — live`:`Connected — test mode`}:{state:`off`,label:`Waiting for the key`};case 5:return t.mode===`live`?{state:`saved`,label:t.webhookConfigured?`Live`:`Live — webhook still to add`}:{state:`off`,label:t.configured?`Still in test mode`:`Later`}}}function Ke(e=5e3){let[t,r]=(0,n.useState)({checked:!1,configured:!1,from:null});return(0,n.useEffect)(()=>{let t=!0,n=async()=>{try{let e=await(await fetch(`/api/email/status`)).json();if(!t)return;r({checked:!0,configured:!!e?.configured,from:e?.from??null})}catch{t&&r(e=>({...e,checked:!0}))}};n();let i=setInterval(n,e);return()=>{t=!1,clearInterval(i)}},[e]),t}function qe(e){return e.checked?e.configured?{state:`saved`,label:`Connected`}:{state:`off`,label:`Not yet`}:{state:`saving`,label:`Checking`}}var Q=`text-accent underline underline-offset-2 font-semibold hover:text-accent-deep transition-colors`;function $({href:e,children:t}){return(0,a.jsx)(`a`,{href:e,target:`_blank`,rel:`noopener noreferrer`,className:Q,children:t})}function Je(){let e=X();return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(J,{number:1,title:`Make a Stripe account`,aside:(0,a.jsx)(t.f,{...Z(1,e)}),children:(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`Open`,` `,(0,a.jsx)(`a`,{href:`https://dashboard.stripe.com`,target:`_blank`,rel:`noopener noreferrer`,className:Q,children:`dashboard.stripe.com`}),` `,`and create an account. It asks about your business — answer what you can and skip what you can't; none of it blocks the next steps.`]}),(0,a.jsxs)(`li`,{children:[`The one thing that matters: near the top right of the Stripe dashboard is a`,` `,(0,a.jsx)(`strong`,{children:`Test mode`}),` toggle. `,(0,a.jsx)(`strong`,{children:`Leave it ON.`}),` While it is on, every card is pretend and every payment is practice — nothing here can cost anyone real money.`]})]})}),(0,a.jsx)(J,{number:2,title:`Copy the secret key`,aside:(0,a.jsx)(t.f,{...Z(2,e)}),children:(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`In the Stripe dashboard, open `,(0,a.jsx)(`strong`,{children:`Developers`}),` (bottom of the left menu), then the `,(0,a.jsx)(`strong`,{children:`API keys`}),` tab.`]}),(0,a.jsxs)(`li`,{children:[`Two keys are listed. The one you want is the `,(0,a.jsx)(`strong`,{children:`Secret key`}),` — click`,` `,(0,a.jsx)(`strong`,{children:`Reveal test key`}),` and copy it. It starts with `,(0,a.jsx)(`strong`,{children:`sk_test_`}),`.`]}),(0,a.jsx)(`li`,{children:`The other one, the Publishable key (pk_test_…), is not needed. Leave it where it is.`})]})}),(0,a.jsxs)(J,{number:3,title:`Paste it into Secrets`,aside:(0,a.jsx)(t.f,{...Z(3,e)}),children:[(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`In Google AI Studio, open the `,(0,a.jsx)(`strong`,{children:`Secrets`}),` tab beside your app and click`,` `,(0,a.jsx)(`strong`,{children:`Add secret`}),`. Copy the name below and paste it as the name, then paste the sk_test_… key you just copied as the value. One row, and that is the whole step.`]}),(0,a.jsx)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:(0,a.jsx)(q,{name:`STRIPE_SECRET_KEY`,hint:`the sk_test_… key you copied in step 2`})}),(0,a.jsx)(`p`,{className:`text-foreground/80`,children:`The chip beside this step turns green on its own once the app can read the key — give it a moment, and reload the app if it doesn't.`})]}),(0,a.jsxs)(J,{number:4,title:`Check it worked`,aside:(0,a.jsx)(t.f,{...Z(4,e)}),children:[(0,a.jsx)(`p`,{className:`text-foreground/80`,children:`Three checks, each a link that opens in a new tab. Nothing to install and nothing to type — if the page that opens says what's described here, it works.`}),(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[(0,a.jsx)($,{href:`/api/_selftest`,children:`Check everything`}),` — a page of checks. A good answer says `,(0,a.jsx)(`strong`,{children:`ok`}),` and lists Stripe as connected in test mode.`]}),(0,a.jsxs)(`li`,{children:[(0,a.jsx)($,{href:`/api/_selftest/checkout`,children:`Try a test payment`}),` — sends you to a real Stripe payment page for a $1 test charge. Pay it with the pretend card number `,(0,a.jsx)(`strong`,{children:`4242 4242 4242 4242`}),`, any future date, any three digits. This check only exists in test mode — it refuses to run against a live key, so it can never take real money.`]}),(0,a.jsxs)(`li`,{children:[(0,a.jsx)($,{href:`/api/_selftest/email`,children:`Send a test email`}),` — sends the test template to your own admin address. This one is about email, not Stripe: if email isn't set up yet, it says so, and that is fine to leave for later.`]})]}),(0,a.jsx)(K,{code:`4242 4242 4242 4242`,label:`Copy the test card number`})]}),(0,a.jsxs)(J,{number:5,title:`Go live, later`,aside:(0,a.jsx)(t.f,{...Z(5,e)}),children:[(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`This step is deliberately last. Do it only when you have watched a test payment go all the way through and you are ready to charge people —`,` `,(0,a.jsx)(`strong`,{children:`from the moment you do it, real cards pay real money.`}),` `,`The easiest way is the `,(0,a.jsx)(`strong`,{children:`Go live`}),` switch at the top of your Payments screen — it walks you through these same steps, collects the live price ids your plans need, and hands you the message that tells your assistant about the switch.`]}),(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80 mb-4`,children:[(0,a.jsxs)(`li`,{children:[`In the Stripe dashboard, switch the `,(0,a.jsx)(`strong`,{children:`Test mode`}),` toggle OFF. If Stripe asks you to `,(0,a.jsx)(`strong`,{children:`activate your account`}),` first — business details, a bank account for payouts — finish that: live payments only run on an activated account.`]}),(0,a.jsxs)(`li`,{children:[`Go back to`,` `,(0,a.jsx)($,{href:`https://dashboard.stripe.com/apikeys`,children:`Developers → API keys`}),` `,`and copy the live Secret key — it starts with `,(0,a.jsx)(`strong`,{children:`sk_live_`}),` instead of sk_test_.`]}),(0,a.jsxs)(`li`,{children:[`In AI Studio's Secrets tab, open `,(0,a.jsx)(`strong`,{children:`STRIPE_SECRET_KEY`}),` and paste the live key over the old value.`]}),(0,a.jsxs)(`li`,{children:[`Add one more secret so receipts and your sale alerts keep sending even when a buyer closes the tab before coming back: in Stripe open`,` `,(0,a.jsx)($,{href:`https://dashboard.stripe.com/webhooks`,children:`Developers → Webhooks`}),` `,`and click `,(0,a.jsx)(`strong`,{children:`Add endpoint`}),`. As the endpoint URL, paste exactly this — it is this app's own address with the webhook route on the end:`,(0,a.jsx)(`div`,{className:`my-2`,children:(0,a.jsx)(K,{code:w(),label:`Copy your webhook address`,compact:!0})}),`When Stripe asks which events to listen for, click`,` `,(0,a.jsx)(`strong`,{children:`Select events`}),`, search for`,` `,(0,a.jsx)(`strong`,{children:`checkout.session.completed`}),` and tick just that one — it is the only event the app needs. Save the endpoint, then copy the`,` `,(0,a.jsx)(`strong`,{children:`Signing secret`}),` it shows (whsec_…) into the row below. (What people bought always shows correctly either way — the app reads Stripe live.)`]})]}),(0,a.jsx)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:(0,a.jsx)(q,{name:`STRIPE_WEBHOOK_SECRET`,hint:`the whsec_… signing secret from the webhook you added`})})]})]})}function Ye({number:e=1}){return(0,a.jsx)(a.Fragment,{children:(0,a.jsxs)(J,{number:e,title:`Email, whenever you like`,aside:(0,a.jsx)(t.f,{...qe(Ke())}),children:[(0,a.jsx)(`p`,{className:`text-foreground/80`,children:`One more pasted key and your app can send email on its own — the receipt a buyer gets, the note that tells you a form came in or something sold. It is separate from Stripe and fine to leave for later; payments work without it.`}),(0,a.jsxs)(`ol`,{className:`space-y-2 list-decimal list-inside text-foreground/80`,children:[(0,a.jsxs)(`li`,{children:[`Open`,` `,(0,a.jsx)(`a`,{href:`https://resend.com`,target:`_blank`,rel:`noopener noreferrer`,className:Q,children:`resend.com`}),` `,`and create a free account — it is the service that delivers email for you.`]}),(0,a.jsxs)(`li`,{children:[`In Resend, open `,(0,a.jsx)(`strong`,{children:`API Keys`}),` and click `,(0,a.jsx)(`strong`,{children:`Create API key`}),`. Copy the key it shows — it starts with `,(0,a.jsx)(`strong`,{children:`re_`}),`.`]}),(0,a.jsx)(`li`,{children:`In AI Studio's Secrets tab, add two rows: the key, and the address owner mail should reach you at.`})]}),(0,a.jsxs)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:[(0,a.jsx)(q,{name:`RESEND_API_KEY`,hint:`the re_… key you just made in Resend`}),(0,a.jsx)(q,{name:`ADMIN_EMAILS`,hint:`your own email address — where sale and form alerts are sent`})]}),(0,a.jsx)(`p`,{className:`text-foreground/80`,children:`Then prove it — the test email goes to your own admin address:`}),(0,a.jsx)(Y,{}),(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`Until you verify a domain of your own in Resend, mail is sent from Resend's shared practice address and tends to land in spam — fine for testing. Before launch, add your domain under `,(0,a.jsx)(`strong`,{children:`Domains`}),` in Resend, then add one more secret with an address at that domain, and your email starts arriving as you.`]}),(0,a.jsx)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:(0,a.jsx)(q,{name:`EMAIL_FROM`,hint:`an address at your verified domain, like hello@yourbusiness.com — optional until then`})})]})})}function Xe(){return(0,a.jsx)(o.Navigate,{to:`/setup`,replace:!0})}Object.defineProperty(exports,`A`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`B`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`C`,{enumerable:!0,get:function(){return ne}}),Object.defineProperty(exports,`D`,{enumerable:!0,get:function(){return oe}}),Object.defineProperty(exports,`E`,{enumerable:!0,get:function(){return se}}),Object.defineProperty(exports,`F`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`H`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`I`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`L`,{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,`M`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`N`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`O`,{enumerable:!0,get:function(){return ce}}),Object.defineProperty(exports,`P`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`R`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`S`,{enumerable:!0,get:function(){return te}}),Object.defineProperty(exports,`T`,{enumerable:!0,get:function(){return ee}}),Object.defineProperty(exports,`U`,{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,`V`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return he}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return Z}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return Y}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return ze}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return De}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return Se}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return we}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return qe}}),Object.defineProperty(exports,`j`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`k`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return K}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return Ee}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return Xe}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return Ke}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return Te}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return Je}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return X}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return Ye}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return We}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,`w`,{enumerable:!0,get:function(){return ae}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return re}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,`z`,{enumerable:!0,get:function(){return u}});
@@ -0,0 +1 @@
1
+ require(`./chunk-DYteiM-1.cjs`);const e=require(`./SetupPayments-Bs57RKA4.cjs`);exports.EmailSetupSteps=e.t,exports.StripeSetupSteps=e.r;