الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - 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>
188 lines
9.5 KiB
Bash
Executable File
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]."
|