⚠️ الطيّ ضرورة أمان لا ترتيب: كل عائلة سيرفرات في links.dart كانت switch بأربعة فروع (Syria · Egypt · Jordan · default)، وفروع سوريا ومصر تشير إلى **سيرفرات سيرو الإنتاجية** (api-syria.siromove.com وغيرها). إبقاؤها في انطلق يعني أن أي جهاز مخزَّن فيه countryCode='Syria' يخاطب إنتاج سيرو مباشرة. انطلق نشرٌ واحد على مضيف واحد (docs/34: سيرفر لكل مستأجر)، والدولة تُحل على السيرفر من GLOBAL_COUNTRY. طُويت 35 كتلة switch إلى قيمة واحدة (11 rider · 11 driver · 6 admin · 7 service)، والخريطة مأخوذة من تعليق docker/nginx/default.conf نفسه («نقطة واحدة لكل الخدمات — التطبيق يغيّر الدومين فقط»): server / rideServer → https://api.intaleqapp.com/backend paymentServer → https://api.intaleqapp.com/v2/main location → …/backend/ride/location locationServerSide(د) → …/loction_server/siro/ride/location سوكيت الركاب → https://api.intaleqapp.com:3030 سوكيت السائقين → https://api.intaleqapp.com:2020 وأيضاً: - appDomain و invite: siromove.com → intaleqapp.com، والمجلد siromove.com/ → intaleqapp.com/ (فيه invite.php الذي يقصده التطبيق). - روابط التنزيل siro.app → intaleqapp.com · بريد السائق المولَّد @siromove.com → @intaleqapp.com · صفحة التتبع · محاكاة تسجيل سوريا. - حُذف getter `syria` الميت من links.dart الثلاثة (لا مستدعي له، وكان يشير إلى إنتاج سيرو). لم يُمسّ عمداً: - `ssl_pinning.dart`: `badCertificateCallback` لا يُستدعى إلا عند فشل التحقق العادي، فشهادة api.intaleqapp.com الصالحة تمرّ بلا مساس. ولا أختلق بصمة شهادة: تُضاف بصمة انطلق بعد إصدار الشهادة (docs/30 G7). - `routesjo.intaleq.xyz` و `map-saas.intaleqapp.com`: خدمات خارجية قائمة بذاتها للمالك (قرار الخرائط المباشرة) لا تُنقل إلى api. - `com.siromove.admin` في userAgentPackageName (5 مواضع): من عائلة هويات المتاجر — كوميت الهويات، وهو يحتاج قرار المالك. ⚠️ يحتاج نشراً على الطرف الآخر: invite.php و track/index.php و /driver على intaleqapp.com. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
6.3 KiB
Dart
190 lines
6.3 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intaleq_driver/constant/box_name.dart';
|
|
import 'package:intaleq_driver/constant/links.dart';
|
|
import 'package:intaleq_driver/controller/functions/crud.dart';
|
|
import '../../main.dart';
|
|
|
|
// ════════════════════════════════════════════
|
|
// نموذج الإحالة
|
|
// ════════════════════════════════════════════
|
|
|
|
class ReferralRecord {
|
|
final String id;
|
|
final String name;
|
|
final String phone;
|
|
final String status; // 'registered', 'active', 'inactive'
|
|
final String type; // 'driver', 'passenger'
|
|
final String joinDate;
|
|
final int tripCount;
|
|
|
|
ReferralRecord({
|
|
required this.id,
|
|
required this.name,
|
|
required this.phone,
|
|
required this.status,
|
|
required this.type,
|
|
required this.joinDate,
|
|
required this.tripCount,
|
|
});
|
|
|
|
factory ReferralRecord.fromJson(Map<String, dynamic> json) {
|
|
return ReferralRecord(
|
|
id: json['id']?.toString() ?? '',
|
|
name: json['name']?.toString() ?? json['nameArabic']?.toString() ?? '',
|
|
phone: json['phone']?.toString() ?? '',
|
|
status: json['status']?.toString() ?? 'registered',
|
|
type: json['type']?.toString() ?? 'driver',
|
|
joinDate: json['created_at']?.toString() ?? '',
|
|
tripCount: int.tryParse(json['trip_count']?.toString() ?? '0') ?? 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════
|
|
// Controller
|
|
// ════════════════════════════════════════════
|
|
|
|
class ReferralController extends GetxController {
|
|
bool isLoading = false;
|
|
List<ReferralRecord> driverReferrals = [];
|
|
List<ReferralRecord> passengerReferrals = [];
|
|
String referralCode = '';
|
|
int totalDriverReferrals = 0;
|
|
int totalPassengerReferrals = 0;
|
|
int activeReferrals = 0;
|
|
double totalRewardsEarned = 0;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_generateReferralCode();
|
|
fetchReferralData();
|
|
}
|
|
|
|
void _generateReferralCode() {
|
|
final driverId = box.read(BoxName.driverID)?.toString() ?? '';
|
|
final name = box.read(BoxName.nameDriver)?.toString() ?? '';
|
|
if (driverId.isNotEmpty) {
|
|
// كود فريد: أول 3 حروف من الاسم + ID
|
|
final prefix = name.length >= 3 ? name.substring(0, 3).toUpperCase() : name.toUpperCase();
|
|
referralCode = '$prefix$driverId';
|
|
}
|
|
}
|
|
|
|
Future<void> fetchReferralData() async {
|
|
isLoading = true;
|
|
update();
|
|
|
|
try {
|
|
// 1. جلب دعوات السائقين
|
|
var driverRes = await CRUD().get(
|
|
link: AppLink.getInviteDriver,
|
|
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
|
);
|
|
if (driverRes != null && driverRes != 'failure') {
|
|
var data = jsonDecode(driverRes);
|
|
if (data['message'] is List) {
|
|
driverReferrals = (data['message'] as List)
|
|
.map((e) => ReferralRecord.fromJson(e))
|
|
.toList();
|
|
totalDriverReferrals = driverReferrals.length;
|
|
}
|
|
}
|
|
|
|
// 2. جلب دعوات الركاب
|
|
var passengerRes = await CRUD().get(
|
|
link: AppLink.getDriverInvitationToPassengers,
|
|
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
|
);
|
|
if (passengerRes != null && passengerRes != 'failure') {
|
|
var data = jsonDecode(passengerRes);
|
|
if (data['message'] is List) {
|
|
passengerReferrals = (data['message'] as List)
|
|
.map((e) => ReferralRecord.fromJson(e))
|
|
.toList();
|
|
totalPassengerReferrals = passengerReferrals.length;
|
|
}
|
|
}
|
|
|
|
// 3. جلب الإحصائيات الدقيقة للمكافآت
|
|
var statsRes = await CRUD().get(
|
|
link: AppLink.getReferralStats,
|
|
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
|
);
|
|
if (statsRes != null && statsRes != 'failure') {
|
|
var data = jsonDecode(statsRes);
|
|
if (data['message'] is List && data['message'].isNotEmpty) {
|
|
var stats = data['message'][0];
|
|
totalRewardsEarned = double.tryParse(stats['totalRewards']?.toString() ?? '0') ?? 0;
|
|
activeReferrals = (int.tryParse(stats['driverInvites']?.toString() ?? '0') ?? 0) +
|
|
(int.tryParse(stats['passengerInvites']?.toString() ?? '0') ?? 0);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ [Referral] Error: $e');
|
|
}
|
|
|
|
isLoading = false;
|
|
update();
|
|
}
|
|
|
|
void copyCode() {
|
|
Clipboard.setData(ClipboardData(text: referralCode));
|
|
}
|
|
|
|
String get shareMessage {
|
|
final appName = 'Siro';
|
|
return 'Join $appName as a driver! Use my code: $referralCode\nDownload: https://intaleqapp.com/driver?ref=$referralCode';
|
|
}
|
|
|
|
String get shareMessagePassenger {
|
|
final appName = 'Siro';
|
|
return 'Get a ride with $appName! Use my code: $referralCode for a discount.\nDownload: https://intaleqapp.com?ref=$referralCode';
|
|
}
|
|
|
|
int get totalReferrals => totalDriverReferrals + totalPassengerReferrals;
|
|
|
|
// ═══════ إرسال دعوة سائق ═══════
|
|
Future<bool> inviteDriver(String phone) async {
|
|
try {
|
|
var res = await CRUD().post(
|
|
link: AppLink.addInviteDriver,
|
|
payload: {
|
|
'driver_id': box.read(BoxName.driverID).toString(),
|
|
'phone': phone,
|
|
},
|
|
);
|
|
if (res != null && res != 'failure') {
|
|
await fetchReferralData();
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ [Referral] Invite driver error: $e');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ═══════ إرسال دعوة راكب ═══════
|
|
Future<bool> invitePassenger(String phone) async {
|
|
try {
|
|
var res = await CRUD().post(
|
|
link: AppLink.addInvitationPassenger,
|
|
payload: {
|
|
'driver_id': box.read(BoxName.driverID).toString(),
|
|
'phone': phone,
|
|
},
|
|
);
|
|
if (res != null && res != 'failure') {
|
|
await fetchReferralData();
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ [Referral] Invite passenger error: $e');
|
|
}
|
|
return false;
|
|
}
|
|
}
|