Skip to content

Commit 25aea5a

Browse files
author
Cohen, Yohay
committed
Implement Telegram bot token configuration in onboarding process, including UI updates for token input and validation. Enhance backend services to handle Telegram configuration updates and ensure proper integration with existing settings. Update localization files for new onboarding strings related to Telegram.
1 parent 5e3c695 commit 25aea5a

8 files changed

Lines changed: 186 additions & 3836 deletions

File tree

client/src/components/onboarding/OnboardingWizard.tsx

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { useQuery } from '@tanstack/react-query';
34
import {
45
ArrowLeft,
56
ArrowRight,
@@ -12,11 +13,10 @@ import {
1213
FolderOpen,
1314
X
1415
} from 'lucide-react';
15-
import { ONBOARDING_STEP_COUNT } from '../../hooks/useOnboardingState';
1616
import { useOnboarding } from '../../contexts/OnboardingContext';
1717
import { useAppLockStatus, useSetupAppLock } from '../../hooks/useAppLock';
1818
import { useEnvConfig, useUpdateEnvConfig, useRestartServer } from '../../hooks/useConfig';
19-
import { getGoogleOAuthCallbackUrl } from '../../lib/api';
19+
import { getApiRoot, getGoogleOAuthCallbackUrl } from '../../lib/api';
2020

2121
export function OnboardingWizard() {
2222
const { t } = useTranslation();
@@ -42,8 +42,20 @@ export function OnboardingWizard() {
4242
const [googleSecret, setGoogleSecret] = useState('');
4343
const [redirectUri, setRedirectUri] = useState('');
4444
const [driveFolder, setDriveFolder] = useState('');
45+
const [telegramToken, setTelegramToken] = useState('');
4546
const [envDirty, setEnvDirty] = useState(false);
4647
const [saveError, setSaveError] = useState<string | null>(null);
48+
const [isSavingTelegram, setIsSavingTelegram] = useState(false);
49+
50+
const { data: telegramConfig } = useQuery({
51+
queryKey: ['telegramConfig', 'onboarding'],
52+
queryFn: async () => {
53+
const res = await fetch(`${getApiRoot()}/telegram/config`);
54+
const data = await res.json();
55+
return data.data as { botToken?: string } | undefined;
56+
},
57+
enabled: step === 0
58+
});
4759

4860
useEffect(() => {
4961
if (!envConfig || envLoading) return;
@@ -55,6 +67,13 @@ export function OnboardingWizard() {
5567
setDriveFolder(envConfig.DRIVE_FOLDER_ID || '');
5668
}, [envConfig, envLoading]);
5769

70+
useEffect(() => {
71+
const tok = telegramConfig?.botToken;
72+
if (tok && !tok.startsWith('***')) {
73+
setTelegramToken(tok);
74+
}
75+
}, [telegramConfig]);
76+
5877
useEffect(() => {
5978
if (step === 3 && !redirectUri) {
6079
setRedirectUri(getGoogleOAuthCallbackUrl());
@@ -63,10 +82,67 @@ export function OnboardingWizard() {
6382

6483
const defaultRedirect = getGoogleOAuthCallbackUrl();
6584

85+
/** Drive folder step only when Google OAuth client ID is set (saved or in form). */
86+
const showDriveStep = useMemo(() => {
87+
const fromEnv = envConfig?.GOOGLE_CLIENT_ID?.trim() || '';
88+
const fromForm = googleId.trim();
89+
return Boolean(fromEnv || fromForm);
90+
}, [envConfig?.GOOGLE_CLIENT_ID, googleId]);
91+
92+
const totalWizardSteps = showDriveStep ? 6 : 5;
93+
const progressCurrent =
94+
showDriveStep || step < 4 ? Math.min(step + 1, totalWizardSteps) : totalWizardSteps;
95+
96+
const advanceAfterGoogleStep = () => {
97+
if (!showDriveStep) {
98+
setStep(5);
99+
} else {
100+
nextStep();
101+
}
102+
};
103+
104+
const onboardingPrevStep = () => {
105+
if (step === 5 && !showDriveStep) {
106+
setStep(3);
107+
} else {
108+
prevStep();
109+
}
110+
};
111+
112+
useLayoutEffect(() => {
113+
if (step === 4 && !showDriveStep) {
114+
setStep(5);
115+
}
116+
}, [step, showDriveStep, setStep]);
117+
66118
const skipEntireSetup = () => {
67119
complete();
68120
};
69121

122+
const handleWelcomeContinue = async () => {
123+
setSaveError(null);
124+
const trimmed = telegramToken.trim();
125+
if (!trimmed || trimmed.includes('***')) {
126+
setStep(1);
127+
return;
128+
}
129+
setIsSavingTelegram(true);
130+
try {
131+
const res = await fetch(`${getApiRoot()}/telegram/config`, {
132+
method: 'POST',
133+
headers: { 'Content-Type': 'application/json' },
134+
body: JSON.stringify({ botToken: trimmed })
135+
});
136+
const data = await res.json();
137+
if (!res.ok) throw new Error(data.error || t('onboarding.save_failed'));
138+
setStep(1);
139+
} catch (e: unknown) {
140+
setSaveError(e instanceof Error ? e.message : t('onboarding.save_failed'));
141+
} finally {
142+
setIsSavingTelegram(false);
143+
}
144+
};
145+
70146
const handleContinueLater = () => {
71147
continueLater();
72148
};
@@ -107,13 +183,13 @@ export function OnboardingWizard() {
107183
if (r) updates.GOOGLE_REDIRECT_URI = r;
108184

109185
if (Object.keys(updates).length === 0) {
110-
nextStep();
186+
advanceAfterGoogleStep();
111187
return;
112188
}
113189
updateEnv(updates, {
114190
onSuccess: () => {
115191
setEnvDirty(true);
116-
nextStep();
192+
advanceAfterGoogleStep();
117193
},
118194
onError: (e: unknown) => {
119195
setSaveError(e instanceof Error ? e.message : t('onboarding.save_failed'));
@@ -166,7 +242,7 @@ export function OnboardingWizard() {
166242
return t(keys[s] || keys[0]);
167243
};
168244

169-
const progressLabel = `${Math.min(step + 1, ONBOARDING_STEP_COUNT)} / ${ONBOARDING_STEP_COUNT}`;
245+
const progressLabel = `${progressCurrent} / ${totalWizardSteps}`;
170246

171247
return (
172248
<div
@@ -201,7 +277,7 @@ export function OnboardingWizard() {
201277
{step === 1 && <Lock className="w-6 h-6" />}
202278
{step === 2 && <KeyRound className="w-6 h-6" />}
203279
{step === 3 && <Cloud className="w-6 h-6" />}
204-
{step === 4 && <FolderOpen className="w-6 h-6" />}
280+
{step === 4 && showDriveStep && <FolderOpen className="w-6 h-6" />}
205281
{step === 5 && <CheckCircle2 className="w-6 h-6" />}
206282
</div>
207283
<div className="min-w-0 flex-1">
@@ -221,9 +297,32 @@ export function OnboardingWizard() {
221297
)}
222298

223299
{step === 0 && (
224-
<div className="rounded-xl bg-slate-50 border border-slate-100 px-4 py-3 flex gap-2 text-sm text-slate-600">
225-
<BookOpen className="w-5 h-5 shrink-0 text-slate-400" />
226-
<p>{t('onboarding.steps.welcome_tip')}</p>
300+
<div className="space-y-4">
301+
<div className="space-y-2">
302+
<label className="text-xs font-bold text-slate-600 block">
303+
{t('onboarding.telegram_token_label')}
304+
</label>
305+
<input
306+
type="password"
307+
value={telegramToken}
308+
onChange={(e) => setTelegramToken(e.target.value)}
309+
placeholder={t('onboarding.telegram_token_placeholder')}
310+
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 text-sm font-mono"
311+
autoComplete="off"
312+
/>
313+
<a
314+
href="https://t.me/BotFather"
315+
target="_blank"
316+
rel="noopener noreferrer"
317+
className="inline-flex text-sm font-bold text-indigo-600 hover:underline"
318+
>
319+
{t('onboarding.open_botfather')}
320+
</a>
321+
</div>
322+
<div className="rounded-xl bg-slate-50 border border-slate-100 px-4 py-3 flex gap-2 text-sm text-slate-600">
323+
<BookOpen className="w-5 h-5 shrink-0 text-slate-400" />
324+
<p>{t('onboarding.steps.welcome_tip')}</p>
325+
</div>
227326
</div>
228327
)}
229328

@@ -350,7 +449,7 @@ export function OnboardingWizard() {
350449
</div>
351450
)}
352451

353-
{step === 4 && (
452+
{step === 4 && showDriveStep && (
354453
<div className="space-y-3">
355454
<label className="text-xs font-bold text-slate-600 block">
356455
{t('onboarding.drive_folder_label')}
@@ -409,7 +508,7 @@ export function OnboardingWizard() {
409508
{step > 0 && (
410509
<button
411510
type="button"
412-
onClick={prevStep}
511+
onClick={onboardingPrevStep}
413512
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl border border-slate-200 text-sm font-bold text-slate-700 hover:bg-white"
414513
>
415514
<ArrowLeft className="w-4 h-4" />
@@ -429,10 +528,11 @@ export function OnboardingWizard() {
429528
</button>
430529
<button
431530
type="button"
432-
onClick={() => setStep(1)}
433-
className="inline-flex items-center gap-1.5 px-5 py-2.5 rounded-xl bg-indigo-600 text-white text-sm font-black hover:bg-indigo-700"
531+
disabled={isSavingTelegram}
532+
onClick={() => void handleWelcomeContinue()}
533+
className="inline-flex items-center gap-1.5 px-5 py-2.5 rounded-xl bg-indigo-600 text-white text-sm font-black hover:bg-indigo-700 disabled:opacity-50"
434534
>
435-
{t('onboarding.get_started')}
535+
{isSavingTelegram ? t('common.loading') : t('onboarding.get_started')}
436536
<ArrowRight className="w-4 h-4" />
437537
</button>
438538
</>
@@ -482,7 +582,7 @@ export function OnboardingWizard() {
482582
<>
483583
<button
484584
type="button"
485-
onClick={() => nextStep()}
585+
onClick={() => advanceAfterGoogleStep()}
486586
className="px-4 py-2 rounded-xl text-sm font-bold text-slate-500 hover:bg-slate-100"
487587
>
488588
{t('onboarding.skip_step')}
@@ -498,7 +598,7 @@ export function OnboardingWizard() {
498598
</button>
499599
</>
500600
)}
501-
{step === 4 && (
601+
{step === 4 && showDriveStep && (
502602
<>
503603
<button
504604
type="button"

client/src/demo/handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ export const demoHandlers = [
189189
})
190190
),
191191

192+
http.post(apiPath('/telegram/config'), () =>
193+
HttpResponse.json({ success: true, message: 'Configuration updated' })
194+
),
195+
192196
http.get(apiPath('/sheets/folder-config'), () =>
193197
HttpResponse.json({ success: true, data: { folderId: '', folderName: '' } })
194198
),

client/src/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,9 @@
958958
"leave_blank_unchanged": "Leave blank if you already saved a secret",
959959
"drive_folder_label": "Google Drive folder ID (optional)",
960960
"drive_folder_placeholder": "ID from the folder URL (after /folders/)",
961+
"telegram_token_label": "Telegram bot token (optional)",
962+
"telegram_token_placeholder": "Paste token from @BotFather",
963+
"open_botfather": "Open @BotFather on Telegram",
961964
"lock_password": "App password",
962965
"lock_confirm": "Confirm password",
963966
"lock_already_configured": "An app password is already set. You can continue to the next step or change it later under the lock banner.",
@@ -978,7 +981,7 @@
978981
"drive_title": "Default Drive folder (optional)",
979982
"done_title": "You are ready",
980983
"welcome_tip": "Tip: use the EN / HE button in the header to switch language anytime.",
981-
"step_0_body": "This app runs on your computer. Your bank data stays local unless you choose to sync with Google.\n\nWe will walk you through optional settings: securing saved profiles, AI (Gemini), Google sign-in, and an optional Drive folder. You can skip anything and finish later.",
984+
"step_0_body": "This app runs on your computer. Your bank data stays local unless you choose to sync with Google.\n\nWe will walk you through optional settings: securing saved profiles, AI (Gemini), Google sign-in, and an optional Drive folder. You can paste a Telegram bot token below (from @BotFather) or add it later under Configuration → Telegram along with allowed user IDs. You can skip anything and finish later.",
982985
"step_1_body": "If you save bank profiles in the app, they are encrypted with a password you choose. This password is not sent to the cloud — it only unlocks your saved credentials on this machine.\n\nMinimum 8 characters. You can skip and enable the lock later from the banner.",
983986
"step_2_body": "Gemini powers automatic categorization of transactions and the financial assistant chat. Without a key, those features stay off; scraping and viewing data still work.\n\nGet a free API key from Google AI Studio and paste it below.",
984987
"step_3_body": "To connect Google Sheets and Google Drive backup, the app needs OAuth credentials from Google Cloud: a Client ID and Client Secret. You must also add the redirect URI below to your OAuth client in Google Cloud — it must match exactly.\n\nYou can skip this and add Google later under Configuration.",

client/src/locales/he.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,9 @@
958958
"leave_blank_unchanged": "השאר ריק אם כבר שמרת סוד",
959959
"drive_folder_label": "מזהה תיקיית Google Drive (אופציונלי)",
960960
"drive_folder_placeholder": "המזהה מכתובת התיקייה (אחרי /folders/)",
961+
"telegram_token_label": "טוקן בוט טלגרם (אופציונלי)",
962+
"telegram_token_placeholder": "הדבק טוקן מ-@BotFather",
963+
"open_botfather": "פתח את @BotFather בטלגרם",
961964
"lock_password": "סיסמת אפליקציה",
962965
"lock_confirm": "אימות סיסמה",
963966
"lock_already_configured": "כבר הוגדרה סיסמת אפליקציה. אפשר להמשיך לשלב הבא או לשנות מאוחר יותר מהבאנר.",
@@ -978,7 +981,7 @@
978981
"drive_title": "תיקיית Drive ברירת מחדל (אופציונלי)",
979982
"done_title": "מוכן לעבודה",
980983
"welcome_tip": "טיפ: השתמש בכפתור EN / HE בכותרת כדי לעבור בין שפות.",
981-
"step_0_body": "האפליקציה רצה במחשב שלך. נתוני הבנק נשארים מקומיים אלא אם תבחר לסנכרן עם Google.\n\nנעבור על הגדרות אופציונליות: אבטחת פרופילים, AI (Gemini), התחברות ל-Google, ותיקיית Drive. אפשר לדלג על כל שלב ולהשלים מאוחר יותר.",
984+
"step_0_body": "האפליקציה רצה במחשב שלך. נתוני הבנק נשארים מקומיים אלא אם תבחר לסנכרן עם Google.\n\nנעבור על הגדרות אופציונליות: אבטחת פרופילים, AI (Gemini), התחברות ל-Google, ותיקיית Drive. אפשר להדביק למטה טוקן בוט טלגרם (מ-@BotFather) או להוסיף מאוחר יותר תחת הגדרות → טלגרם יחד עם מזהי משתמשים מורשים. אפשר לדלג על כל שלב ולהשלים מאוחר יותר.",
982985
"step_1_body": "אם שומרים פרופילי בנק באפליקציה, הם מוצפנים בסיסמה שאתה בוחר. הסיסמה לא נשלחת לענן — רק פותחת את הפרטים השמורים במחשב זה.\n\nלפחות 8 תווים. אפשר לדלג ולהפעיל נעילה מאוחר יותר מהבאנר.",
983986
"step_2_body": "Gemini מפעיל סיווג אוטומטי של תנועות וצ'אט פיננסי. בלי מפתח, התכונות האלה כבויות; גריפה וצפייה בנתונים עדיין עובדות.\n\nקבל מפתח API חינמי מ-Google AI Studio והדבק למטה.",
984987
"step_3_body": "לחיבור Google Sheets וגיבוי ל-Drive נדרשים אישורי OAuth מ-Google Cloud: Client ID ו-Client Secret. יש להוסיף ב-Google Cloud את כתובת ההפניה למטה — חייבת להתאים בדיוק.\n\nאפשר לדלג ולהוסיף Google מאוחר יותר תחת הגדרות.",

docker-compose.yml

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
version: '3.8'
2-
31
services:
42
app:
53
image: ghcr.io/yohaybn/israeli-bank-scraper-docker-app:master
64
restart: unless-stopped
7-
ports:
8-
- "3000:3000"
9-
volumes:
10-
# Map local data directory to container /data directory
11-
# This will persist settings, results, and logs
12-
- ./data:/data
13-
environment:
14-
- PORT=3000
15-
- DATA_DIR=/data
16-
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
17-
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
18-
- DRIVE_FOLDER_ID=${DRIVE_FOLDER_ID}
19-
- APP_SECRET=${APP_SECRET}
20-
- GEMINI_API_KEY=${GEMINI_API_KEY}
5+
ports:
6+
- "3000:3000"
7+
volumes:
8+
# Map local data directory to container /data directory
9+
# This will persist settings, results, and logs
10+
- ./data:/data
11+
environment:
12+
- PORT=3000
13+
- DATA_DIR=/data

lint_output.txt

-1.95 KB
Binary file not shown.

0 commit comments

Comments
 (0)