QR_STAND_PRESETS (packages/shared) references qr-1..qr-9 (skipping
qr-7), but apps/web/public only had qr-1,2,4,5,6.svg — qr-3, qr-8,
and qr-9 404'd wherever the raw preset list renders before/instead of
a DB-driven override. Built to match the existing qr-1.svg structure
(layer-background/layer-icons/qr-placeholder/layer-texts, same 400x650
canvas, QR box at translate(90,215) 220x220 — verified this aligns
with QrStandGeneratorClient's hardcoded 33.08%/55% overlay position),
using each preset's own color palette from qr-stands.ts.
RevenueCat's entitlement identifier contains U+2024 (ONE DOT LEADER),
not a plain period — "Menul.io Pro" never matched the real
"Menul⁚io Pro" key, so isProActive() stayed false even after a real
purchase (confirmed via a genuine sandbox trial that RevenueCat
recorded as active while the app kept showing "Ücretsiz plan").
RevenueCat's API doesn't allow renaming an entitlement's lookup_key
after creation (405), so fixed on our side instead: this app only
ever grants one entitlement, so "any active entitlement" == Pro,
no identifier string needed.
eas.json's production build profile hardcodes its own env block —
local apps/mobile/.env is never read by EAS cloud builds. The
RevenueCat public key was missing from that list, so every
production/TestFlight build compiled EXPO_PUBLIC_REVENUECAT_API_KEY
as undefined, making configurePurchases() silently no-op and every
later Purchases call throw "no singleton instance".
"There is no singleton instance" when tapping Yayınla — the app can
land directly on /menu (e.g. iOS restoring the last screen on
relaunch) without ever running index.tsx's routing gate, so
Purchases.configure() was never called. Menu editor's load() now
configures it defensively too, same pattern as account/subscription.tsx.
Missing safe-area top inset pushed the header (back arrow + title)
up under the status bar/notch, overlapping the clock and making the
button effectively untappable. Also falls back to router.replace
when there's no back history instead of silently no-op'ing.
Publishing (including re-publishing after a lapsed subscription —
PRD §15) now checks Pro entitlement first; if inactive, prompts the
owner to the paywall instead of calling the publish endpoint. The
paywall also flags packages with an active introductory offer as
"7 gün ücretsiz dene" (both plans now have a 7-day free trial
configured in App Store Connect, Turkey territory).
- Purchases.configure() runs with appUserID = restaurantId at every
point the id becomes known (login resolve, cold-start rehydration,
onboarding restaurant creation) — this must match what the backend
webhook expects (see apps/api/src/routes/subscription.ts).
- New account/subscription.tsx paywall: current entitlement status,
offering packages with real store prices, purchase, restore.
- Linked from the account screen.
Native module — requires an EAS/dev-client build, not plain Expo Go.
Verifies the shared-secret Authorization header, then maps
RevenueCat's app_user_id (the restaurant id — mobile must configure
Purchases with appUserID: restaurantId) to an upsert into
subscriptions. INITIAL_PURCHASE/RENEWAL/PRODUCT_CHANGE/UNCANCELLATION
-> active, BILLING_ISSUE -> grace_period, EXPIRATION -> suspended.
CANCELLATION alone does not suspend — it only means auto-renew is
off, access continues until the period actually expires.
Tested end-to-end against the real Supabase project (create
restaurant, POST the webhook, verify the subscriptions row).
Removes the per-item confidence field end to end (prompt, extraction
parsing, ai_import_items writes, API responses, low-confidence badge
in the mobile review screen). ai_import_items.confidence column is
left in place but unused (nullable, no destructive migration).
Adding a custom domain now:
- creates a Cloudflare Custom Hostname (SSL for SaaS) automatically,
returning the ownership + SSL validation TXT records to show the
restaurant owner alongside the CNAME instruction
- adds the domain to the Coolify app's fqdn and triggers a restart
(Coolify only regenerates Traefik labels on deploy, not on a bare
fqdn PATCH — coollabsio/coolify#6281)
- /verify now checks Cloudflare's actual ssl.status instead of doing
a DNS CNAME lookup, which is structurally blind on proxied hostnames
Tested end-to-end against the real Cloudflare zone and Coolify app
(create, verify data, then clean up) before this push — see
docs/PROGRESS.md.
Requires CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, COOLIFY_API_TOKEN,
COOLIFY_BASE_URL, COOLIFY_WEB_APP_UUID in the API's environment.
Without them, domain add/verify falls back to DB-only bookkeeping.
- domains.ts: a proxied Cloudflare custom hostname's real CNAME target
is never visible to a plain DNS lookup, so a re-verify always fails
and was silently downgrading already-verified domains back to
pending. Only a first-time check can now land on pending.
- restaurants.ts: PUT /restaurants/:id/theme referenced an undeclared
themeData variable (pre-existing tsc error on main, not introduced
here) — added the missing themes-by-key lookup it depended on.
Root-path requests to a subdomain or custom domain rewrote to
/menu/<slug>/ (trailing slash), which the [slug] dynamic route can't
match — the app fell through to notFound() even for verified,
published restaurants. Custom domains (e.g. ad.ayris.tech) always hit
this since their only real traffic is the root path.
Default 1 MiB limit rejected base64-encoded camera photos before the
request handler ever ran ("Request body is too large"), unrelated to
the OpenAI model. Also adds the public domains RLS policy needed for
custom-domain resolution on the public menu page (already applied
manually via SQL Editor, committing for history).