החנות של בייקס, בכיס של הלקוחותפיתוח אפליקציה לחנות אונליין קיימת
אפליקציית אנדרואיד ב־React Native שמדברת עם חנות Laravel קיימת. אותה עגלה, אותם מחירים, אותו מועדון. בלי שכלל עסקי אחד שוכפל.
- React Native 0.88
- Expo SDK 58
- Laravel 13
- Sanctum
- FCM HTTP v1

01. האתגראפליקציה אמיתית, בלי לשכפל את החנות
בייקס מוכרת ציוד וחומרי גלם לאפייה. האתר, שנבנה ב־Laravel 13 עם Filament 5, כבר ניהל קטלוג עם וריאציות (צבעים, גדלים), מועדון לקוחות עם דרגות, גיפטכארד, ותשלום ב־CreditGuard. הבקשה: אפליקציה שמרגישה כמו אפליקציה, ולא כמו אתר בתוך מסגרת.
הסכנה בפרויקט כזה היא לא הקוד של המסכים. הסכנה היא שהאפליקציה מתחילה לחשב מחירים, קופונים ומשלוח חינם בעצמה, ואז יש שתי חנויות שמתרחקות זו מזו. לכן כל החלטה כאן התחילה מאותה שאלה: מי הבעלים של הכלל הזה, האתר או האפליקציה. התשובה כמעט תמיד הייתה האתר.
native ולא עטיפה
ניווט מקורי, גלילה חלקה, RTL מלא, אנימציות ב־Reanimated. אותו מראה של המותג.
מקור אמת אחד
מחירים, מלאי, קופונים, משלוח חינם ודרגות מועדון מחושבים רק בשרת. האפליקציה מציגה, לא מחשבת.
התשלום נשאר באתר
האפליקציה לעולם לא רואה כרטיס אשראי. סליקה אחת לתחזק, ובלי In-App Purchase של אפל.
התראות שמכבדות את הלקוחות
לא בשבת, לא בלילה, לא יותר משתיים בשבוע. הודעה שאסור לשלוח עכשיו נדחית, לא נזרקת.
וגם: סביבת הפיתוח
הבנייה נעשית על מחשב Windows עם שם משתמש בעברית, בלי Mac, ועם מגבלת 260 תווים בנתיבים. זה נשמע כמו פרט, ועלה יום עבודה שלם. הסיפור בהמשך.
02. ארכיטקטורהאפליקציה דקה, שרת שמחליט
האפליקציה מדברת עם האתר רק דרך /api/v1. היא לא יודעת איך מחשבים מחיר. היא שולחת "הוסף לעגלה" ומקבלת בחזרה עגלה מחושבת. שלושה מסלולים עיקריים עוברים בין הצדדים:
גלישה ועגלה
בקשות JSON עם Bearer token ו־X-Install-Id. השרת מחזיר Presenters עם רשימה לבנה, לא מודלים. מחיר לעולם לא נקרא מהלקוח.
תשלום
POST /cart/checkout מחזיר כתובת עם קוד חד־פעמי (10 דקות). היא נפתחת ב־Custom Tab, מאמצת את העגלה בדפדפן ומעבירה ל־/checkout הרגיל. הכרטיס נשאר מול CreditGuard.
התראות
אירועי הזמנה ועגלה נכנסים ל־PushNotifier, עוברים מדיניות זמנים, נשלחים מתור ישירות ל־FCM HTTP v1. בלי SDK ובלי שירות צד שלישי.
03. טכנולוגיהמה בנוי עם מה
האפליקציה
- React Native 0.88 · React 19.3
- Expo SDK 58 · expo-router (ניווט לפי קבצים)
- TanStack Query 5 (מטמון ובקשות)
- Reanimated 4 · Gesture Handler · worklets
- expo-secure-store (Keystore) · expo-notifications
- expo-image · expo-web-browser (Custom Tabs)
- TypeScript · Jest
השרת
- Laravel 13 · Filament 5 · Livewire 4
- Sanctum: טוקן גישה וטוקן רענון
- MariaDB · תורים ו־Scheduler
- FCM HTTP v1 ישיר, JWT חתום ב־RS256
- CreditGuard לסליקה
- PHPUnit: 2,258 בדיקות בסוויטה המלאה
- Docker על VPS
בנייה ופרסום
- Android Studio · Gradle · Expo prebuild
- ninja 1.12.1 + LongPathsEnabled
- חבילה il.co.bakes · versionCode 13
- npm run version:bump: גרסה, build ו־CHANGELOG ביחד
- נכסי Google Play נוצרו מרינדור של האפליקציה עצמה
04. איך נבנהאיך זה נבנה, שלב אחרי שלב
הכול מתועד בתאריכים, כי כל שלב יצא כחבילת שרת עם בדיקות וכגרסת אפליקציה עם CHANGELOG. מ־build 1 ל־build 13 עברו פחות משבוע.
- 20.10pkg 21 · build 1
ה־API והאפליקציה הראשונה
החלטה על אפליקציה אחת לאנדרואיד ואייפון, מול /api/v1 בלבד. אבטחת טוקנים, קשירה למכשיר, תשלום ב־handoff. 98 בדיקות API עם 340 טענות. מסכים: בית, קטגוריות, מוצר, עגלה, חשבון.
- 20.10Android Studio
המלחמה עם Windows
שם משתמש בעברית שבר את Gradle (prefab). נתיב של 278 תווים שבר את ninja. הפתרון: SDK ב־C:\Android\Sdk, פרויקט ב־C:\b, LongPathsEnabled ו־ninja 1.12.1. אחרי זה שם החבילה הוחלף ל־il.co.bakes, לפני ההעלאה הראשונה, כי אחריה הוא נעול.
- 20.10Google Play
נכסי החנות מהאפליקציה עצמה
תיאורים, אייקון, גרפיקה ו־6 צילומי טלפון, 4 טאבלט ו־4 דסקטופ, כולם נבדקו אוטומטית מול דרישות Play. הצילומים הם רינדור אמיתי של הקוד, ולכן אי אפשר שיתיישנו.
- 21.10pkg 27 · build 8–9
התראות פוש
FCM ישיר מ־Laravel, מדיניות שבת ושעות, שני ערוצי אנדרואיד, מסך הגדרות, קמפיינים עם קהלים, תבניות הזמנה שנערכות בפאנל, והודעת "יש גרסה חדשה".
- 22.10pkg 28 · 1.0.15 · build 11
קנה עכשיו וסיכום הזמנה
כפתורי הוספה לעגלה על כרטיסי מוצר, "קנה עכשיו" עם עגלה זמנית נפרדת שלא נוגעת בעגלה של הלקוח, ומסך סיכום הזמנה לפני התשלום.
- 23.10pkg 30 · 1.0.16 · build 12
עגלה נטושה ופוש שפותח את המסך הנכון
רצף תזכורות במייל ובפוש: שעה 6, 30, 54, ועוצרים ב־72. לחיצה על התראה פותחת עגלה, הזמנה או דף תשלום, גם מהפעלה קרה.
- 24.10pkg 39 · 1.0.17 · build 13
המועדון נכנס למסך הבית
הבאנר מזהה חבר מועדון מחובר והופך ל"המועדון שלי" עם הנקודות. אותה בקשה, מצב אחר, לפי הטוקן.
05. המסכיםהאפליקציה האמיתית, לא מוקאפ
כל צילום כאן הוא מסך חי של הגרסה הנוכחית. לחיצה על מסך מגדילה אותו.
גלישה וחיפוש
מוצר וקנייה
מועדון וחשבון
התראות
אותו מסך, שלושה מצבים
הבאנר של המועדון לא קבוע באפליקציה. הוא נקרא מ־/home, והשרת מחליט אם להציג "בקרוב", "פעיל" או "המועדון שלי", לפי מצב המועדון והטוקן של הלקוח. תשובה לאורח נשמרת במטמון ציבורי, תשובה לחבר ב־private, ו־Vary: Authorization מונע ערבוב ביניהן.
state: soonstate: livestate: memberצילומי המסך הם רינדור של קוד האפליקציה האמיתי (גרסה 1.0.17) מול הקטלוג הציבורי של בייקס. שם המשתמשת, הנקודות והעגלה בדוגמה.
06. החלטותהחלטות שכדאי להכיר
התשלום נשאר באתר
האפליקציה מבקשת POST /cart/checkout ומקבלת כתובת /app/checkout/‹48 תווים›. הקוד חד־פעמי, נשמר במטמון רק כ־sha256, לא מכיל את טוקן העגלה ולא מחבר לחשבון. פתיחת הקישור מאמצת את העגלה בדפדפן ומעבירה לצ׳קאאוט הרגיל.
שלוש סיבות: האפליקציה לא רואה כרטיס, יש סליקה אחת לתחזק, ואפל לא דורשת In-App Purchase על מוצרים פיזיים. קוד שגוי או ישן מוביל לעגלה עם משפט, לא לשגיאה.
CheckoutHandoffController.php$token = $handoff->redeem($code);
$cart = $token ? Cart::query()->where('token', $token)->first() : null;
if (! $cart || $cart->items()->doesntExist()) {
return $this->private(redirect()->route('cart')
->with('notice', 'הקישור לתשלום כבר לא בתוקף. אפשר להמשיך מכאן.'));
}
CartManager::adopt($cart->token);
return $this->private(redirect()->route('checkout'));טוקנים שקשורים למכשיר
שני טוקנים: גישה ל־30 דקות, ורענון ל־60 יום שהוא חד־פעמי ומתחלף. שימוש חוזר בטוקן רענון ישן מחזיר 401 ומנתק את המכשיר. כל בקשה נושאת X-Install-Id, וטוקן שהועתק לטלפון אחר נדחה.
בצד האפליקציה, הקליינט מרענן פעם אחת בלבד גם כשחמש בקשות נכשלות באותה שנייה. רענון שנדחה מוציא את המשתמש. רענון שלא הגיע לשרת לא מוציא אף אחד.
EnsureTokenMatchesDevice.phpif (! $device || ! hash_equals((string) $device->install_id, $info->installId)) {
return response()->json([
'code' => 'session_invalid',
'message' => 'ההתחברות כבר לא בתוקף. יש להתחבר מחדש.',
], 401);
}src/api/client.tsasync request<T>(method: string, path: string, o: Options = {}): Promise<T> {
const auth = o.auth ?? true;
let res = await this.send(method, path, o, auth);
if (res.status === 401 && auth && this.d.session.tokens) {
const refreshed = await this.refresh();
if (refreshed) res = await this.send(method, path, o, auth);
}
return this.parse<T>(res);
}מסך בית שהשרת מרכיב
/home מחזיר סליידר, באנר מועדון ובאנר גיפטכארד מאותן הגדרות שהאתר קורא. אין קמפיין שמשוכפל לאפליקציה: שינוי בפאנל מופיע בפתיחה הבאה.
שני מתגים בפאנל (club.home_banner, giftcard.home_banner) מכבים כל באנר. בקשה עם טוקן לא נכנסת למטמון משותף.
HomeController.phpreturn response()->json(['data' => [
'slides' => $this->slides(),
'club' => setting('club.home_banner', true) ? $this->club($club, $user) : null,
'gift_card' => setting('giftcard.home_banner', true) ? $this->giftCard() : null,
]])
->header('Cache-Control', $user ? 'private, max-age=60' : 'public, max-age=120')
->header('Vary', 'Authorization');מדיניות פוש: דוחים, לא מוותרים
שבת סגורה לכול, מיום שישי 14:00 עד מוצאי שבת 21:00. הצעות רק בין 09:00 ל־20:00 ולא יותר משתיים בשבוע לטלפון. "ההזמנה מוכנה" יוצאת בכל שעה שהיא נכונה, חוץ משבת. הודעת עדכון גרסה היא היחידה שיוצאת מיד.
הפונקציה לא מסרבת. היא מחזירה את הרגע הבא שמותר, והשליחה נדחית אליו. הלוגיקה מכוסה בבדיקות.
PushPolicy.php// A few passes settle every combination (Shabbat end → before 09:00 → …).
for ($i = 0; $i < 6; $i++) {
$moved = $this->afterShabbat($at);
if ($category === 'promos' || $category === 'cart') {
$moved = $this->withinHours($moved, ...$this->hoursFor($category));
$moved = $this->afterShabbat($moved);
}
if ($moved->equalTo($at)) {
return $at;
}
$at = $moved;
}"קנה עכשיו" בלי לגעת בעגלה
עגלה זמנית היא שורה רגילה בטבלת העגלות עם is_buy_now. האתר שומר את העגלה האמיתית בצד, ו־middleware מחזיר אותה ברגע שהלקוח עוזב את הצ׳קאאוט לכל עמוד אחר. תמונות, סקריפטים ו־prefetch לא נחשבים יציאה.
תזכורות עגלה נטושה ודוחות מדלגים על עגלות כאלה, כדי שהמספרים לא יזדהמו.
עגלה נטושה, ברצף ובנימוס
תזכורת ראשונה אחרי 6 שעות, אחר כך כל 24, ועוצרים אחרי 72. רק בין 09:00 ל־20:00, לא בשבת, ולא ללקוח ששילם בינתיים. עגלה שנגעו בה אחרי תזכורת נחשבת נטישה חדשה.
הסימון נעשה לפני השליחה (stamp-before-send), כך ששליחה כפולה לא יכולה לקרות גם אם הג׳וב רץ פעמיים. בדרך התגלה באג בקישור של הזמנה לא שולמה, והוא תוקן עם קישור חתום ופג תוקף.
Windows: מה שלא כתוב במדריכים
שם משתמש בעברית נכנס ל־sdk.dir ושבר את prefab. הפתרון: SDK ב־C:\Android\Sdk ו־GRADLE_USER_HOME קצר. אחר כך ninja נכשל על נתיב של 278 תווים, כי ninja 1.10 שמגיע עם CMake 3.22 לא תומך בנתיבים ארוכים. הפתרון: LongPathsEnabled ו־ninja 1.12.1 במקומו.
כל זה הפך לארבעה סקריפטים בתיקיית הפרויקט, כדי שהבנייה הבאה תהיה פקודה אחת ולא יום חקירה.
PowerShellninja: error: mkdir(…/react_codegen_rngesturehandler_codegen.dir/C_/b/node_modules/
react-native-gesture-handler/shared/shadowNodes/react/renderer/components/…)
fix-hebrew-paths.ps1 SDK → C:\Android\Sdk, GRADLE_USER_HOME
build-in-short-path.ps1 project copy → C:\b
enable-long-paths.ps1 LongPathsEnabled=1 + ninja 1.12.1
refresh-build-copy.ps1 clean copy after a package rename07. הפאנלמה שהחנות עורכת בעצמה
כל מה שהאפליקציה מציגה או שולחת נשלט מפאנל Filament של האתר, בלי מפתח באמצע. תבניות ההתראות, קמפיינים עם קהל ותזמון, והגדרות המועדון.
08. מספריםהעבודה במספרים
הסוויטה גדלה עם המוצר
מספר הבדיקות בסוויטת השרת המלאה, כפי שתועד בכל חבילה. 499 בדיקות נוספו תוך 12 יום.
09. לקחיםמה למדנו
הכלל נשאר במקום אחד
כל פעם שהתפתינו לחשב משהו באפליקציה, התברר שהאתר כבר מחשב אותו. האפליקציה הפכה לדקה, והשרת לשלם.
נתוני אמת בצילומי המסך
החנות לא נבנתה לפי מוקאפ. צילומי החנות נוצרו מקוד האפליקציה, ולכן הם מתעדכנים עם כל גרסה.
נימוס הוא פיצ׳ר
מדיניות שבת, תקרה שבועית ותזכורות שנעצרות כשהלקוח שילם הן מה שהופך התראות מרעש לשירות.
הבנייה היא חלק מהמוצר
ארבעה סקריפטים ל־Windows חסכו יום לכל גרסה עתידית. מי שבונה אפליקציה בונה גם את הדרך לבנות אותה שוב.
לבדוק מה לא נבדק
בכל חבילה כתבנו מה לא אומת: משלוח אמיתי של פוש, תשלום אמיתי, מכשיר אמיתי. הרשימה הזו הקדימה בעיות יותר מכל בדיקה.
מה הבא
- אייפון: בנייה ב־EAS בלי Mac, כשיהיה חשבון ארגון
- מסך מועדון בתוך האפליקציה, במקום מעבר לאתר
- חזרה אוטומטית מהתשלום לאפליקציה (App Links)
- Play Integrity ופוש "חזר למלאי"
איך מפתחים אפליקציה לחנות אונליין שכבר עובדת?
הסכנה בפרויקט כזה אינה הקוד של המסכים. הסכנה היא שהאפליקציה מתחילה לחשב מחירים, קופונים ומשלוח חינם בעצמה, ואז יש שתי חנויות שמתרחקות זו מזו. לכן בבייקס כל החלטה התחילה בשאלה אחת: מי הבעלים של הכלל, האתר או האפליקציה. התשובה כמעט תמיד הייתה האתר. האפליקציה שולחת "הוסף לעגלה" ומקבלת בחזרה עגלה מחושבת, והיא לא יודעת איך מחשבים מחיר. כך שינוי במחיר, במלאי או בקופון מופיע בשני המקומות באותו רגע, וזו הגישה שלנו גם בפיתוח אפליקציות לעסקים אחרים.
מבחינה טכנית, האפליקציה מדברת עם האתר רק דרך נקודות קצה של API, והשרת מחזיר אובייקטים מסוננים ברשימה לבנה ולא מודלים גולמיים. בסך הכול נבנו 40 נקודות קצה, שמכסות קטלוג, עגלה, חשבון והתראות. את חנות האינטרנט של בייקס בנינו קודם, ובזכות זה האפליקציה יכלה להישען על קטלוג, מועדון ומחירים שכבר עבדו. אם אתם שוקלים אפליקציה לחנות קיימת, כדאי להתחיל באפיון קצר של הכללים העסקיים, כדי להחליט מראש מה נשאר בשרת.
למה התשלום נשאר באתר ולא בתוך האפליקציה?
באפליקציית בייקס התשלום מתבצע בעמוד התשלום המאובטח של האתר. האפליקציה מבקשת כתובת תשלום, מקבלת קישור עם קוד חד-פעמי שתקף 10 דקות, ופותחת אותו בדפדפן המערכת. הקוד נשמר בשרת רק כ-sha256, לא מכיל את טוקן העגלה ולא מחבר לחשבון, והפתיחה מאמצת את העגלה ומעבירה לצ׳קאאוט הרגיל. האפליקציה לעולם לא רואה כרטיס אשראי, ויש סליקה אחת לתחזק. קוד ישן או שגוי מוביל לעגלה עם הסבר, ולא למסך שגיאה.
יש לכך יתרון נוסף: מוצרים פיזיים לא מחייבים In-App Purchase של אפל, ולכן אין צורך לוותר על הסליקה הקיימת. אותו עיקרון חל על שאר החלטות האבטחה: טוקן גישה ל-30 דקות, טוקן רענון חד-פעמי ל-60 יום, וקשירה של כל בקשה למכשיר, כך שטוקן שהועתק לטלפון אחר נדחה. את אתר המסחר עצמו אנחנו מריצים על אחסון, אבטחה ותחזוקה מסודרים.
איך בונים התראות פוש שלא מציקות ללקוחות?
בבייקס ההתראות נשלחות ישירות מהשרת אל Firebase Cloud Messaging, בלי שירות צד שלישי באמצע, ועוברות לפני כן מדיניות זמנים. שבת סגורה לכול, מיום שישי 14:00 עד מוצאי שבת 21:00. הצעות נשלחות רק בין 09:00 ל-20:00 ולא יותר משתיים בשבוע לטלפון. ההחלטה היא לדחות ולא לוותר: הפונקציה מחזירה את הרגע הבא שמותר, והשליחה נדחית אליו. עגלה נטושה מקבלת תזכורות אחרי 6, 30 ו-54 שעות ונעצרת אחרי 72, ולא נשלחת ללקוח ששילם בינתיים.
את התבניות והקמפיינים עורך צוות החנות בעצמו מפאנל הניהול: כותרת וטקסט לכל אירוע הזמנה, קהל, תמונה, יעד בתוך האפליקציה ותזמון. לחיצה על התראה פותחת את המסך הנכון, עגלה, הזמנה או דף תשלום, גם מהפעלה קרה. כל זה נבדק בסוויטת בדיקות שגדלה ל-2,258 בדיקות. לעלות משוערת של אפליקציה כזו אפשר להתחיל במחשבון העלות, או לקבוע שיחת אפיון.
מאמרים שכדאי לקרוא בהמשך
שאלות נפוצות
איזו טכנולוגיה נבחרה לאפליקציית בייקס ולמה?
האם האפליקציה מחשבת מחירים בעצמה?
האם אפשר לשלם בכרטיס אשראי בתוך האפליקציה?
איך מגנים על חשבון הלקוח בטלפון?
האם האפליקציה קיימת גם לאייפון?
כמה זמן לקח לבנות את האפליקציה?
איך מתחילים פרויקט של אפליקציה לחנות קיימת?
רוצים אפליקציה לחנות שלכם?
Logicode בונה אפליקציות שמתחברות למערכות שכבר עובדות: חנויות, מועדונים, ניהול. בלי להתחיל מאפס ובלי לשכפל את העסק.