Skip to content

Commit cbe4036

Browse files
committed
refactor(ui/dev): unify bottom bar and stabilize local start workflow
Consolidate platform-specific bottom bar behavior into a shared structure and add resilient local startup orchestration so Expo and Vercel can run together with reliable DB migration retries and clean shutdown on Ctrl+C. Made-with: Cursor
1 parent 9bbf482 commit cbe4036

14 files changed

+955
-701
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<u>**In progress, contribute!**</u>
66

7-
This program is built upon [React Native](https://reactnative.dev/) by Meta and [Expo](https://expo.dev) multiplatform technologies, Windows build and executable creation achieved with [Electron Builder](https://www.electron.build/) and [Electron Forge](https://www.electronforge.io/), working in Telegram with help of [Telegram Mini Apps React SDK](http://telegram-mini-apps.com/), [Bot API](https://core.telegram.org/bots) and [Grammy](https://grammy.dev/). AI is backed by [OpenAI API](https://openai.com/ru-RU/api/), blockchain info is processed from [Swap.Coffee API](https://docs.swap.coffee/eng/user-guides/welcome). DB for the best user's experience we host on [Neon](https://neon.tech/).
7+
This program is built upon [React Native](https://reactnative.dev/) by Meta and [Expo](https://expo.dev) multiplatform technologies, Windows build and executable creation achieved with [Electron Builder](https://www.electron.build/) and [Electron Forge](https://www.electronforge.io/), working in Telegram with help of [Telegram Mini Apps React SDK](http://telegram-mini-apps.com/), [Bot API](https://core.telegram.org/bots) and [Grammy](https://grammy.dev/). AI is backed by [OpenAI API](https://openai.com/ru-RU/api/), blockchain info is processed from [Swap.Coffee API](https://docs.swap.coffee/eng/user-guides/welcome). DB for the best user's experience we host on [Neon](https://neon.tech/). Keys are managed by [Google Cloud Key Management](https://cloud.google.com/security/products/security-key-management).
88

99
Check out our [Pitch Deck](./PitchDeck/PitchDeck.md).
1010

app/_layout.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ import { AuthProvider } from "../auth/AuthContext";
66
import { TelegramProvider, useTelegram } from "../ui/components/Telegram";
77
import { GlobalLogoBarWithFallback } from "../ui/components/GlobalLogoBarWithFallback";
88
import { GlobalBottomBar } from "../ui/components/GlobalBottomBar";
9-
import { GlobalBottomBarWeb } from "../ui/components/GlobalBottomBarWeb";
109
import { useColors } from "../ui/theme";
1110
import { useEffect, useRef } from "react";
1211

1312
/**
1413
* Three-block column layout (same as Flutter):
1514
* 1. Logo bar (optional in TMA when not fullscreen)
1615
* 2. Main area (flex, scrollable per screen) – Stack updates on route change
17-
* 3. [Web only] Raw HTML textarea test (compare with GlobalBottomBar in TMA)
18-
* 4. AI & Search bar (fixed at bottom)
16+
* 3. AI & Search bar (fixed at bottom, platform-specific internals)
1917
*/
2018
export default function RootLayout() {
2119
useOtaUpdateChecks();
@@ -109,14 +107,10 @@ function RootContent() {
109107
<View style={styles.main}>
110108
<Stack screenOptions={{ headerShown: false }} />
111109
</View>
112-
{Platform.OS === "web" ? (
113-
// Avoid mounting textarea/DOM mirror before theme — kills dark flash from RN-web inputs.
114-
!useTelegramTheme || themeBgReady ? (
115-
<GlobalBottomBarWeb />
116-
) : null
117-
) : (
118-
<GlobalBottomBar />
119-
)}
110+
{
111+
// Avoid mounting web internals before theme — kills dark flash from RN-web inputs.
112+
Platform.OS !== "web" || !useTelegramTheme || themeBgReady ? <GlobalBottomBar /> : null
113+
}
120114
</View>
121115
);
122116
}

backlogs/short_term_backlog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
Login screen
2+
Dealing with pool count
13
Change picture in Windows installer
24
Wallet: Telegram, Connected, Unhosted<br>
35
Username loaded without intermediate state<br>

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
"forge": "./windows/forge/forge.config.js"
2626
},
2727
"scripts": {
28-
"prestart": "npm run db:migrate",
28+
"prestart": "node scripts/migrate-db-retry.mjs",
29+
"prestart:full": "node scripts/migrate-db-retry.mjs",
2930
"build": "expo export -p web",
30-
"start": "npx concurrently -n expo,bot,vercel \"expo start\" \"npx tsx scripts/run-bot-local.ts\" \"npm run dev:vercel\"",
31+
"start": "node scripts/start-expo-managed.mjs",
32+
"start:full": "npx concurrently -n expo,bot,vercel \"expo start\" \"npx tsx scripts/run-bot-local.ts\" \"npm run dev:vercel\"",
3133
"start:expo": "expo start",
3234
"reset-project": "node ./scripts/reset-project.js",
3335
"android": "expo start --android",

scripts/migrate-db-retry.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { spawn } from "node:child_process";
2+
3+
const MAX_ATTEMPTS = 3;
4+
const RETRY_DELAY_MS = 3000;
5+
6+
function sleep(ms) {
7+
return new Promise((resolve) => setTimeout(resolve, ms));
8+
}
9+
10+
function runMigrate() {
11+
return new Promise((resolve) => {
12+
const child = spawn("npm", ["run", "db:migrate"], {
13+
stdio: "inherit",
14+
shell: true,
15+
env: process.env,
16+
});
17+
18+
child.on("close", (code) => {
19+
resolve(code ?? 1);
20+
});
21+
});
22+
}
23+
24+
async function main() {
25+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
26+
// Keep logs explicit so startup failures are easy to debug in terminals/CI.
27+
console.log(`[db:migrate:retry] attempt ${attempt}/${MAX_ATTEMPTS}`);
28+
const code = await runMigrate();
29+
30+
if (code === 0) {
31+
console.log("[db:migrate:retry] migrations succeeded");
32+
process.exit(0);
33+
}
34+
35+
if (attempt < MAX_ATTEMPTS) {
36+
console.warn(
37+
`[db:migrate:retry] attempt ${attempt} failed (exit ${code}), retrying in ${RETRY_DELAY_MS}ms...`,
38+
);
39+
await sleep(RETRY_DELAY_MS);
40+
continue;
41+
}
42+
43+
console.error(
44+
`[db:migrate:retry] failed after ${MAX_ATTEMPTS} attempts, aborting startup`,
45+
);
46+
process.exit(code);
47+
}
48+
}
49+
50+
void main();

scripts/start-expo-managed.mjs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { spawn, spawnSync } from "node:child_process";
2+
import path from "node:path";
3+
4+
const EXPO_PORT = "8081";
5+
6+
function killProcessTree(pid) {
7+
return new Promise((resolve) => {
8+
if (!pid) {
9+
resolve();
10+
return;
11+
}
12+
13+
if (process.platform === "win32") {
14+
const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
15+
stdio: "ignore",
16+
});
17+
killer.on("close", () => resolve());
18+
return;
19+
}
20+
21+
try {
22+
process.kill(pid, "SIGTERM");
23+
} catch {
24+
// Ignore: process may already be gone.
25+
}
26+
resolve();
27+
});
28+
}
29+
30+
function killProcessTreeSync(pid) {
31+
if (!pid) return;
32+
33+
if (process.platform === "win32") {
34+
spawnSync("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
35+
stdio: "ignore",
36+
});
37+
return;
38+
}
39+
40+
try {
41+
process.kill(pid, "SIGTERM");
42+
} catch {
43+
// Ignore: process may already be gone.
44+
}
45+
}
46+
47+
async function main() {
48+
const expoCliPath = path.resolve(process.cwd(), "node_modules", "expo", "bin", "cli");
49+
const vercelCliPath = path.resolve(process.cwd(), "node_modules", "vercel", "dist", "index.js");
50+
const expo = spawn(process.execPath, [expoCliPath, "start", "--port", EXPO_PORT], {
51+
stdio: "inherit",
52+
env: process.env,
53+
});
54+
const vercel = spawn(process.execPath, [vercelCliPath, "dev", "--yes"], {
55+
stdio: "inherit",
56+
env: {
57+
...process.env,
58+
SKIP_DB_MIGRATE: "1",
59+
TS_NODE_PROJECT: "api/tsconfig.json",
60+
},
61+
});
62+
const children = [expo, vercel];
63+
64+
let shuttingDown = false;
65+
const shutdown = async (exitCode = 0) => {
66+
if (shuttingDown) return;
67+
shuttingDown = true;
68+
await Promise.all(children.map((child) => killProcessTree(child.pid)));
69+
process.exit(exitCode);
70+
};
71+
72+
process.on("exit", () => {
73+
for (const child of children) {
74+
killProcessTreeSync(child.pid);
75+
}
76+
});
77+
process.on("SIGINT", () => {
78+
void shutdown(130);
79+
});
80+
process.on("SIGTERM", () => {
81+
void shutdown(143);
82+
});
83+
84+
expo.on("exit", (code) => {
85+
if (shuttingDown) return;
86+
void shutdown(code ?? 0);
87+
});
88+
vercel.on("exit", (code) => {
89+
if (shuttingDown) return;
90+
void shutdown(code ?? 0);
91+
});
92+
}
93+
94+
void main();

texts/header-footer-adaptation.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Header/Footer adaptation by page and device
2+
3+
Yes - you can (and should) make header and footer behavior depend on both **route** and **runtime** (web, native, Telegram Mini App).
4+
5+
This note describes a minimal, scalable pattern for this repository.
6+
7+
---
8+
9+
## 1) Goal
10+
11+
Keep one global app shell, but allow each page group to define:
12+
13+
- whether header is shown
14+
- whether footer is shown
15+
- which variant is used (full, compact, hidden)
16+
- device-specific behavior (web vs native vs TMA)
17+
18+
---
19+
20+
## 2) Current baseline in this repo
21+
22+
`app/_layout.tsx` currently always renders:
23+
24+
- `GlobalLogoBarWithFallback` at top
25+
- page `<Stack />` in the middle
26+
- `GlobalBottomBarWeb` (web) or `GlobalBottomBar` (native/TMA) at bottom
27+
28+
This is simple, but it means welcome/auth pages inherit the same bars as app pages.
29+
30+
---
31+
32+
## 3) Recommended structure (minimal and clear)
33+
34+
Use route-group layouts as shell boundaries:
35+
36+
- `app/(auth)/_layout.tsx` -> auth shell (welcome/login flow)
37+
- `app/(app)/_layout.tsx` -> app shell (home/ai/settings/etc.)
38+
39+
Then:
40+
41+
1. Keep `app/_layout.tsx` for providers only (`TelegramProvider`, `AuthProvider`, theme setup).
42+
2. Move top/bottom bars out of root and into the group layout(s) that need them.
43+
3. In each group layout, select variants by device/platform.
44+
45+
This keeps file structure small while preventing UI condition spaghetti in one file.
46+
47+
---
48+
49+
## 4) Adaptation matrix
50+
51+
| Route group | Web | Native app | Telegram Mini App |
52+
|---|---|---|---|
53+
| `(auth)` | Header optional (brand only), footer hidden | Header optional, footer hidden | Header optional, footer hidden |
54+
| `(app)` | Full header + web footer/search | Full header + native footer/search | TMA-aware header + native footer/search |
55+
56+
Notes:
57+
58+
- Welcome screen is usually cleaner with no footer controls.
59+
- App pages should keep navigation/actions visible and consistent.
60+
- In TMA, preserve Telegram theme sync and avoid pre-theme flashes.
61+
62+
---
63+
64+
## 5) How to implement (practical)
65+
66+
### A. Auth layout (`app/(auth)/_layout.tsx`)
67+
68+
- Render a `Stack` with `headerShown: false`.
69+
- If needed, render a minimal top brand component only.
70+
- Do not mount bottom bar.
71+
72+
### B. App layout (`app/(app)/_layout.tsx`)
73+
74+
- Keep auth guard redirect to `/welcome`.
75+
- Render top bar + content + bottom bar.
76+
- Branch footer by `Platform.OS` (web/native), same as current root logic.
77+
78+
### C. Optional per-page override
79+
80+
If a specific page needs no bars (for example, fullscreen flow), add route metadata in code:
81+
82+
- simple approach: maintain a tiny set of route names in group layout (`hiddenHeaderRoutes`, `hiddenFooterRoutes`)
83+
- cleaner later: create a `ScreenChromeContext` with `{ header: "full" | "compact" | "none", footer: ... }`
84+
85+
Do not over-engineer before multiple special pages actually exist.
86+
87+
---
88+
89+
## 6) Theme and color consistency rules
90+
91+
1. All screens use `useColors()` (`background`, `primary`, `secondary`).
92+
2. Header/footer must read from the same theme source.
93+
3. Avoid hard-coded light/dark literals in page components.
94+
4. For TMA, keep `themeBgReady` guard behavior to prevent flash/mismatch.
95+
96+
---
97+
98+
## 7) Suggested near-term rollout
99+
100+
1. Keep current providers in `app/_layout.tsx`.
101+
2. Move header/footer rendering from root to `app/(app)/_layout.tsx`.
102+
3. Keep `app/(auth)/_layout.tsx` minimal (no footer).
103+
4. Leave welcome blank now, but already under `(auth)` so future auth UI is isolated.
104+
5. Add one shared `PageContainer` helper later if repeated spacing/styling appears.
105+
106+
---
107+
108+
## 8) One-line decision
109+
110+
Use **group-based shell adaptation**: auth routes get a minimal chrome, app routes get full chrome, and device-specific variants are selected inside each group layout using the same `useColors()` theme contract.
111+

0 commit comments

Comments
 (0)