your-ai-workflow-firebase-os 1.13.2 → 1.13.4
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/dist/SetupPayments-DmTAoyVP.cjs +1 -0
- package/dist/SetupPayments-ETo-2G0g.cjs +389 -0
- package/dist/{SetupPayments-Co-IHDuy.js → SetupPayments-za4FDlCG.js} +590 -534
- package/dist/your-ai-workflow-firebase-os.cjs.js +16 -16
- package/dist/your-ai-workflow-firebase-os.css +1 -1
- package/dist/your-ai-workflow-firebase-os.es.js +879 -842
- package/package.json +1 -1
- package/dist/SetupPayments-Bs57RKA4.cjs +0 -389
- package/dist/SetupPayments-C-zpS99n.cjs +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require(`./chunk-DYteiM-1.cjs`);const e=require(`./SetupPayments-ETo-2G0g.cjs`);exports.EmailSetupSteps=e.t,exports.StripeSetupSteps=e.r;
|
|
@@ -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 O(e);return typeof t==`string`?t:(C.goTo(t.url),null)}async function O(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 te(){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 ne(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 k=null;function re(){return k||=fetch(`/api/payments/status`).then(e=>e.json()).then(e=>({configured:e?.configured===!0&&e?.enabled!==!1,mode:e?.mode??null})).catch(()=>(k=null,{configured:!1,mode:null})),k}function ie(){k=null}function ae(){let[e,t]=(0,n.useState)({loading:!0,configured:!1,mode:null});return(0,n.useEffect)(()=>{let e=!1;return re().then(n=>{e||t({loading:!1,...n})}),()=>{e=!0}},[]),e}function oe(){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 se(){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 ce(...e){return t.Dt(t.Ot(e))}var A=.72,j=({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,A)).toFixed(2)}px`,`important`),a<A&&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:ce(`page-title w-fit inline-block tracking-tight mb-3 mt-1 text-gradient`,e),...r,children:t})};j.displayName=`PageTitle`;var le=e=>e.trim().split(`-`).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``),M=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).replace(/([A-Z])([A-Z][a-z])/g,`$1-$2`).toLowerCase(),N=e=>{let t=e.replace(/-/g,` `);return t.charAt(0).toUpperCase()+t.slice(1)},ue=(()=>{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(M))).sort()})(),P=`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(`.`),F={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`]},de=(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]},fe=(e,t)=>{let n=0;for(let r of t)r===e[n]&&n++;return n===e.length};function I(e,t=96,n=ue){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 F[e]||[])s(t,70);if(e.endsWith(`s`))for(let t of F[e.slice(0,-1)]||[])s(t,70)}if(o.size===0&&r.length>=3)for(let e of n)e.split(`-`).some(e=>de(r,e)<=(r.length>5?2:1))?s(e,40):r.length>=4&&fe(r,e.replace(/-/g,``))&&s(e,20);let c=e=>{let t=P.indexOf(e);return t===-1?P.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 pe(e){if(!e)return t.En;let n=e.trim(),r=t.jt;return r[le(n)]||r[n]||t.En}function L({name:e,className:t}){return(0,n.createElement)(pe(e),{className:t})}function me({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)(()=>I(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)(L,{name:l,className:`w-4 h-4`})}),(0,a.jsx)(`span`,{className:`text-[13px] font-semibold truncate`,children:N(M(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=M(l).toLowerCase()===e;return(0,a.jsx)(`button`,{type:`button`,title:N(e),"aria-label":N(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)(L,{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 he=/(<[A-Z][A-Za-z0-9]*(?:\s[^<>]*)?\/?>|src\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]|'[A-Za-z0-9_@./-]+')/g,ge=/(https?:\/\/[^\s]+)/g,R=e=>e.split(ge).flatMap((e,t)=>t%2==1?[{text:e,kind:`value`}]:e.split(he).map((e,t)=>({text:e,kind:t%2==1?`code`:void 0}))).filter(e=>e.text),_e=/exactly: |Here is (?:what|how) [^:]*: |It should ask for: |Where the form belongs on the page: |a button that says /,ve=e=>{let t=_e.exec(e);if(!t)return R(e);let n=t.index+t[0].length,r=e.slice(0,n),i=e.slice(n),a=i.indexOf(` — `);return[...R(r),{text:a===-1?i:i.slice(0,a),kind:`value`},...R(a===-1?``:i.slice(a))]},ye=`text-accent font-semibold bg-accent/[0.08] rounded-[4px] px-1 -mx-[1px] [overflow-wrap:anywhere]`,be=`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 z({text:e}){return(0,a.jsx)(a.Fragment,{children:ve(e).map((e,t)=>e.kind?(0,a.jsx)(`span`,{className:e.kind===`value`?ye:be,children:e.text},t):(0,a.jsx)(n.default.Fragment,{children:e.text},t))})}function xe({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)(z,{text:e})},t))}):(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[`“`,(0,a.jsx)(z,{text:i}),`”`]})})]})}var Se=`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:`${Se} 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:`${Se} 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,Ae=Object.getOwnPropertySymbols,je=Object.prototype.hasOwnProperty,Me=Object.prototype.propertyIsEnumerable,B=(e,t,n)=>t in e?ke(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,V=(e,t)=>{for(var n in t||={})je.call(t,n)&&B(e,n,t[n]);if(Ae)for(var n of Ae(t))Me.call(t,n)&&B(e,n,t[n]);return e},Ne=(0,n.forwardRef)((e,t)=>{let r=V(V({},n.useContext(Oe)),e);return n.createElement(`svg`,V({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`}))}),Pe=`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
|
+
`,Fe=`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
|
+
`,Ie=n.default.lazy(()=>Promise.resolve().then(()=>require(`./SetupPayments-DmTAoyVP.cjs`)).then(e=>({default:e.StripeSetupSteps}))),Le=n.default.lazy(()=>Promise.resolve().then(()=>require(`./SetupPayments-DmTAoyVP.cjs`)).then(e=>({default:e.EmailSetupSteps}))),H=[{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 Re(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 U(e,t){let n=Re(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 ze(e){let t=[],n=[],r=t=>U(t,e);for(let{key:e,label:i}of H){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 Be(){return ze().allSet}var Ve=Fe;function He(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 Pe.replace(/(function adminEmails\(\) \{\s*return )\[[^\]]*\];/,`$1[${n}];`)}function W(e){return e.split(/[,\s]+/).map(e=>e.trim()).filter(Boolean)}var Ue=/^[^@\s]+@[^@\s]+\.[^@\s]+$/;function G({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 We(e){let t=``;try{t=new URL(e).hostname}catch{t=``}let n=t===`localhost`||t===`127.0.0.1`||t.endsWith(`.local`);return{host:t,local:n}}function Ge(){let{host:e,local:t}=We(typeof window>`u`?``:window.location.href);return e?(0,a.jsxs)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl bg-background flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3 px-4 py-3 mt-3`,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:t?`this machine — Firebase trusts it already, nothing to add`:`your app's address, read from this page`})]}),!t&&(0,a.jsx)(G,{compact:!0,code:e,label:`Copy the address`})]}):null}function K({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 Ke({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=W(r);if(e.length===0){d(`Type an email address first — it is what the rules will name.`);return}let t=e.filter(e=>!Ue.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(He(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)(G,{compact:!0,code:o,label:`Copy the rules`})]})]})}function q({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 qe({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=W(U(`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)(j,{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)(Ne,{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)(q,{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)(q,{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)(`li`,{children:[(0,a.jsx)(`strong`,{children:`Ignore the storageBucket it shows here.`}),` Storage does not exist yet, so that one is a guess and it is wrong. The real address appears in step 6, the moment you create storage — that is the one your app needs.`]})]})}),(0,a.jsxs)(q,{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.jsxs)(`p`,{className:`text-foreground/80`,children:[`Six rows, and that is all of them. Fill five of them now and leave`,` `,(0,a.jsx)(`strong`,{children:`VITE_FIREBASE_STORAGE_BUCKET`}),` empty — step 6 gives you its real value. Your own email address is not one of them: you type that straight into step 5 in a moment.`]}),(0,a.jsxs)(`p`,{className:`text-foreground/80`,children:[(0,a.jsx)(`strong`,{children:`Do not press Apply yet.`}),` One value is still missing, and Apply is the last thing you press on this whole page.`]}),(0,a.jsx)(`div`,{className:`glass-panel border border-[var(--panel-border)] rounded-2xl overflow-hidden bg-background`,children:H.map(({key:e,shownAs:t})=>(0,a.jsx)(K,{name:e,shownAs:t,hint:e===`VITE_FIREBASE_STORAGE_BUCKET`?`leave this one empty — step 6 gives you the real address`:void 0},e))})]}),(0,a.jsxs)(q,{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:`Your app's address is right here`}),` — copy it below, then in Firebase click `,(0,a.jsx)(`strong`,{children:`Add domain`}),`, paste it, and save. Nothing to hunt for: it is the address this very page is being served from.`]})]}),(0,a.jsx)(Ge,{})]}),(0,a.jsxs)(q,{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)(Ke,{initialEmails:o})]}),(0,a.jsxs)(q,{number:6,title:`Create storage, publish its rules, and press Apply`,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.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.jsxs)(`li`,{children:[`Now Firebase shows the storage address at the top of the page — something like`,` `,`your-project.firebasestorage.app. `,(0,a.jsx)(`strong`,{children:`Copy that`}),`. This is the real one; the one Firebase showed you back in step 2 was a guess made before storage existed.`]}),(0,a.jsxs)(`li`,{children:[`Back in AI Studio's `,(0,a.jsx)(`strong`,{children:`Secrets`}),` tab, paste it into`,` `,(0,a.jsx)(`strong`,{children:`VITE_FIREBASE_STORAGE_BUCKET`}),` — the row you left empty.`]}),(0,a.jsxs)(`li`,{children:[(0,a.jsx)(`strong`,{children:`Press Apply.`}),` That is the last thing to press. Then reload this page and your app is connected.`]})]}),(0,a.jsx)(G,{code:Ve,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)(Ie,{})})})]}),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)(Le,{number:s.payments?6:1})})})]})]})})]})}function J({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 Je={checked:!1,configured:!1,mode:null,webhookConfigured:!1};function Y(e=5e3){let[t,r]=(0,n.useState)(Je);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 X(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 Z(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 Ye(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 Xe(){let e=Y();return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(q,{number:1,title:`Make a Stripe account`,aside:(0,a.jsx)(t.f,{...X(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)(q,{number:2,title:`Copy the secret key`,aside:(0,a.jsx)(t.f,{...X(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)(q,{number:3,title:`Paste it into Secrets`,aside:(0,a.jsx)(t.f,{...X(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)(K,{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)(q,{number:4,title:`Check it worked`,aside:(0,a.jsx)(t.f,{...X(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)(G,{code:`4242 4242 4242 4242`,label:`Copy the test card number`})]}),(0,a.jsxs)(q,{number:5,title:`Go live, later`,aside:(0,a.jsx)(t.f,{...X(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)(G,{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)(K,{name:`STRIPE_WEBHOOK_SECRET`,hint:`the whsec_… signing secret from the webhook you added`})})]})]})}function Ze({number:e=1}){return(0,a.jsx)(a.Fragment,{children:(0,a.jsxs)(q,{number:e,title:`Email, whenever you like`,aside:(0,a.jsx)(t.f,{...Ye(Z())}),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)(K,{name:`RESEND_API_KEY`,hint:`the re_… key you just made in Resend`}),(0,a.jsx)(K,{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)(J,{}),(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)(K,{name:`EMAIL_FROM`,hint:`an address at your verified domain, like hello@yourbusiness.com — optional until then`})})]})})}function Qe(){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 te}}),Object.defineProperty(exports,`D`,{enumerable:!0,get:function(){return ae}}),Object.defineProperty(exports,`E`,{enumerable:!0,get:function(){return oe}}),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 se}}),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 O}}),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 me}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return X}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return J}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return Be}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return De}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return xe}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return we}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return Ye}}),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 G}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return Ee}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return Qe}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return Z}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return Te}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return Xe}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return Y}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return Ze}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return qe}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return I}}),Object.defineProperty(exports,`w`,{enumerable:!0,get:function(){return ie}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return ne}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,`z`,{enumerable:!0,get:function(){return u}});
|