Files
tripz-llc/scripts/generate-tenant-app.sh
T
Hamza-AyedandClaude Fable 5 de4bbd00ac feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
  يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
  `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
  قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.

N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.

فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
  التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:06:46 +03:00

188 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# ==========================================================================
# توليد تطبيق مستأجر واحد من مانيفست الباك إند (docs/22 — N3).
#
# سكربت واحد يأخذ slug المستأجر → يجلب مانيفسته → يولّد BuildConfig بأعلام
# `const` (طبقات lite/pro/max — docs/22 §1.5) → يضبط bundle ID والأيقونات
# والـsplash من اللوغو → يبني ويدفع عبر Shorebird.
#
# الاستعمال:
# PLATFORM_SECRET=xxx API=https://tripz-api.intaleqapp.com/api \
# ./scripts/generate-tenant-app.sh <slug> <rider|driver> [--build]
#
# بلا `--build` يتوقّف بعد التوليد (تفتيشٌ بلا بناء) — افتراض متعمَّد: البناء
# يستهلك دقائق ويحتاج بيئة فلاتر كاملة، فلا يُطلق ضمناً.
#
# ⚠️ الخطوات الموسومة [Q] تعتمد على مشروع فلاتر (المجموعة Q) غير المكتمل بعد.
# ==========================================================================
set -euo pipefail
SLUG="${1:?الاستعمال: generate-tenant-app.sh <slug> <rider|driver> [--build]}"
APP_KIND="${2:-rider}" # rider | driver
DO_BUILD="${3:-}"
API="${API:?اضبط API=https://tripz-api.intaleqapp.com/api}"
SECRET="${PLATFORM_SECRET:?اضبط PLATFORM_SECRET}"
case "$APP_KIND" in
rider|driver) ;;
*) echo "✗ نوع التطبيق يجب أن يكون rider أو driver (وصل: $APP_KIND)"; exit 1 ;;
esac
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$ROOT/apps/$APP_KIND"
WORK="$ROOT/.build/$SLUG-$APP_KIND"
echo "▶ توليد تطبيق [$APP_KIND] للمستأجر [$SLUG]"
# ── 1. جلب المانيفست من الباك إند ──────────────────────────────────────
echo " • جلب المانيفست…"
TENANT_ID="$(curl -fsSL -H "x-platform-secret: $SECRET" "$API/admin/tenants" \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const t=JSON.parse(s).find(x=>x.slug==='$SLUG');if(!t){console.error('مستأجر غير موجود: $SLUG');process.exit(1)}process.stdout.write(t.id)})")"
MANIFEST="$(curl -fsSL -H "x-platform-secret: $SECRET" "$API/admin/tenants/$TENANT_ID/app-manifest")"
val() { echo "$MANIFEST" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{process.stdout.write(String(JSON.parse(s).$1 ?? ''))})"; }
APP_NAME="$(val app_name)"
BID_ANDROID="$(val bundle_id_android)"
BID_IOS="$(val bundle_id_ios)"
COUNTRY="$(val country_pack)"
echo " الاسم: $APP_NAME | أندرويد: $BID_ANDROID | دولة: $COUNTRY"
[ -z "$BID_ANDROID" ] && { echo " ✗ bundle_id_android غير مضبوط — اضبطه من لوحة السوبر-أدمن أولاً"; exit 1; }
# ── 2. نسخة عمل من مشروع فلاتر ─────────────────────────────────────────
if [ ! -d "$APP_DIR/lib" ]; then
echo " ⚠️ [Q] مشروع فلاتر $APP_DIR غير جاهز بعد — أحفظ المانيفست فقط."
mkdir -p "$ROOT/.build"
echo "$MANIFEST" > "$ROOT/.build/$SLUG-$APP_KIND.manifest.json"
echo "✅ حُفظ المانيفست في .build/$SLUG-$APP_KIND.manifest.json"
exit 0
fi
rm -rf "$WORK"; mkdir -p "$(dirname "$WORK")"; cp -r "$APP_DIR" "$WORK"
# ── 3. توليد BuildConfig بأعلام const ⭐ (docs/22 §1.5) ─────────────────
# جوهر الطبقات: علم `const` قيمته false يُحذف كوده من الـbinary عند بناء AOT
# (tree-shaking). لو قرأنا الأعلام من Map وقت التشغيل بقي كود كل الميزات في
# الملف وأمكن استخراجه — وضاع الغرض كله.
echo " • توليد BuildConfig بأعلام const…"
mkdir -p "$WORK/lib/core"
echo "$MANIFEST" | node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
const m = JSON.parse(s);
const q = (v) => JSON.stringify(v ?? '');
const feats = m.features || {};
const camel = (k) => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
const flags = Object.keys(feats).sort()
.map(k => ' static const bool ' + camel(k) + ' = ' + (feats[k] === true) + ';')
.join('\n');
const lim = m.limits || {};
const out = [
'// ⚠️ ملف مُولَّد — لا تُحرّره. مصدره scripts/generate-tenant-app.sh',
'// من مانيفست الباك إند. الأعلام const عمداً: قيمة false يُحذف كودها من',
'// الـbinary عند بناء AOT (docs/22 §1.5). لا تحوّلها إلى Map وقت تشغيل.',
'',
'class BuildConfig {',
' static const String tenantSlug = ' + q(m.slug) + ';',
' static const String tenantName = ' + q(m.name) + ';',
' static const String appName = ' + q(m.app_name) + ';',
' static const String countryPack = ' + q(m.country_pack) + ';',
' static const String bundleIdAndroid = ' + q(m.bundle_id_android) + ';',
' static const String bundleIdIos = ' + q(m.bundle_id_ios) + ';',
'',
' /// وسائل الدفع المبنيّة. المفعّل فعلياً = هذه ∩ المانيفست الريموت.',
' static const List<String> paymentMethods = ' + JSON.stringify(m.payment_methods || []) + ';',
'',
' /// الحدود العددية — للعرض فقط؛ الفرض على السيرفر (docs/19 K4).',
' static const int driversMax = ' + (lim.drivers_max || 0) + ';',
' static const int citiesMax = ' + (lim.cities_max || 0) + ';',
'}',
'',
'/// أعلام الميزات المخبوزة في هذا البناء.',
'/// الفعّال = هذه ∩ المانيفست الريموت ∩ استحقاقات السيرفر (docs/22 §1.5).',
'class Features {',
flags,
'}',
'',
].join('\n');
process.stdout.write(out);
});
" > "$WORK/lib/core/build_config.dart"
echo " ✓ lib/core/build_config.dart"
# ── 4. ضبط bundle ID (أندرويد + iOS) ───────────────────────────────────
echo " • ضبط bundle ID…"
GRADLE="$WORK/android/app/build.gradle"
[ -f "$GRADLE.kts" ] && GRADLE="$GRADLE.kts"
if [ -f "$GRADLE" ]; then
# sed -i يختلف بين BSD (ماك) وGNU (لينكس) — نمرّر لاحقة نسخ احتياطي صراحةً
# ثم نحذفها، وهي الصيغة الوحيدة التي تعمل على الاثنين.
sed -i.bak -E "s/applicationId[[:space:]]*=?[[:space:]]*\"[^\"]*\"/applicationId \"$BID_ANDROID\"/" "$GRADLE"
rm -f "$GRADLE.bak"
echo " ✓ applicationId → $BID_ANDROID"
else
echo " ⚠️ لم أجد build.gradle — تخطّي"
fi
PBX="$WORK/ios/Runner.xcodeproj/project.pbxproj"
if [ -n "$BID_IOS" ] && [ -f "$PBX" ]; then
sed -i.bak -E "s/PRODUCT_BUNDLE_IDENTIFIER = [^;]*;/PRODUCT_BUNDLE_IDENTIFIER = $BID_IOS;/g" "$PBX"
rm -f "$PBX.bak"
echo " ✓ PRODUCT_BUNDLE_IDENTIFIER → $BID_IOS"
fi
# ── 5. الأيقونات و splash من اللوغو ────────────────────────────────────
echo " • الأيقونات و splash…"
mkdir -p "$WORK/assets"
if curl -fsSL "$API/tenant/logo/$SLUG" -o "$WORK/assets/logo.png" 2>/dev/null; then
# الباك إند تحقّق من المقاس والتربيع عند الرفع (docs/22 — N2)، فما يصل هنا
# صالحٌ لتوليد الأيقونات بلا تشويه.
cat > "$WORK/flutter_launcher_icons.yaml" <<EOF
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/logo.png"
remove_alpha_ios: true
EOF
cat > "$WORK/flutter_native_splash.yaml" <<EOF
flutter_native_splash:
color: "#FFFFFF"
image: assets/logo.png
android_12:
image: assets/logo.png
EOF
echo " ✓ لوغو المستأجر + إعدادات الأيقونات/splash"
else
echo " ⚠️ لا لوغو مرفوع — يُستعمل الافتراضي (ارفعه من لوحة السوبر-أدمن)"
fi
# ── 6. اسم التطبيق الظاهر ──────────────────────────────────────────────
MANIFEST_XML="$WORK/android/app/src/main/AndroidManifest.xml"
if [ -n "$APP_NAME" ] && [ -f "$MANIFEST_XML" ]; then
sed -i.bak -E "s/android:label=\"[^\"]*\"/android:label=\"$APP_NAME\"/" "$MANIFEST_XML"
rm -f "$MANIFEST_XML.bak"
echo " • اسم التطبيق → $APP_NAME"
fi
# ── 7. البناء + Shorebird (اختياري) ────────────────────────────────────
if [ "$DO_BUILD" != "--build" ]; then
echo ""
echo "✅ اكتمل التوليد في: $WORK"
echo " للبناء أضف --build (يحتاج flutter + shorebird مثبّتين)."
exit 0
fi
echo " • الأيقونات والبناء…"
cd "$WORK"
flutter pub get
dart run flutter_launcher_icons || echo " ⚠️ تخطّي الأيقونات"
dart run flutter_native_splash:create || echo " ⚠️ تخطّي splash"
# ⚠️ الطبقة الأصيلة موحّدة عبر كل المستأجرين عمداً (docs/22 §1.5): بهذا تصير
# ترقية الباقة patch عبر Shorebird بلا مراجعة متجر. لا تُفرِّع الأصيل هنا.
shorebird release android \
--dart-define=BASE_URL="$API" \
--dart-define=TENANT="$SLUG"
echo "✅ اكتمل بناء [$APP_KIND] لـ[$SLUG]."