#!/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 [--build] # # بلا `--build` يتوقّف بعد التوليد (تفتيشٌ بلا بناء) — افتراض متعمَّد: البناء # يستهلك دقائق ويحتاج بيئة فلاتر كاملة، فلا يُطلق ضمناً. # # ⚠️ الخطوات الموسومة [Q] تعتمد على مشروع فلاتر (المجموعة Q) غير المكتمل بعد. # ========================================================================== set -euo pipefail SLUG="${1:?الاستعمال: generate-tenant-app.sh [--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 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" < "$WORK/flutter_native_splash.yaml" <