Compare commits

...

7 Commits

Author SHA1 Message Date
Hamza-Ayed
5f62455113 Update: 2026-05-25 21:44:11 2026-05-25 21:44:11 +03:00
Hamza-Ayed
2f1a6f9c85 Update: 2026-05-17 18:47:51 2026-05-17 18:47:51 +03:00
Hamza-Ayed
9ad361e992 Update: 2026-05-16 01:40:56 2026-05-16 01:40:56 +03:00
Hamza-Ayed
aceb7d324f Update: 2026-05-16 01:36:22 2026-05-16 01:36:22 +03:00
Hamza-Ayed
24a9f064a1 Update: 2026-05-16 00:33:22 2026-05-16 00:33:22 +03:00
Hamza-Ayed
663896becb Update: 2026-05-16 00:27:57 2026-05-16 00:27:59 +03:00
Hamza-Ayed
e93f1d4f34 feat: implement annual subscription model across backend quota system and flutter UI 2026-05-16 00:15:38 +03:00
25 changed files with 793 additions and 447 deletions

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info
$stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ?
@@ -57,10 +57,12 @@ final class QuotaMiddleware
json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403);
}
// Auto-reset monthly counter if billing period has ended
// Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+30 days'));
$cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare("
UPDATE subscriptions
@@ -76,15 +78,15 @@ final class QuotaMiddleware
$sub['current_period_start'] = $newStart;
$sub['current_period_end'] = $newEnd;
error_log("QuotaMiddleware: Auto-reset monthly counter for tenant {$tenantId}");
error_log("QuotaMiddleware: Auto-reset annual counter for tenant {$tenantId}");
}
// Check invoice quota
$used = (int)$sub['invoices_used_this_month'];
$limit = (int)$sub['max_invoices_per_month'];
$limit = (int)$sub['max_invoices_per_month']; // Keeping the DB column name the same for compatibility
if ($used >= $limit) {
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
'quota_type' => 'invoices',
'used' => $used,
'limit' => $limit,

View File

@@ -9,12 +9,14 @@
return [
'free' => [
'id' => 'free',
'name_ar' => 'مجانية',
'name_en' => 'Free',
'name_ar' => 'التجربة المجانية',
'name_en' => 'Free Trial',
'max_companies' => 1,
'max_invoices_month' => 15,
'max_users' => 1,
'price_jod' => 0.00,
'price_monthly_jod' => 0.00,
'price_annual_jod' => 0.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'gray',
@@ -29,90 +31,50 @@ return [
],
'basic' => [
'id' => 'basic',
'name_ar' => 'أساسية',
'name_en' => 'Basic',
'name_ar' => 'الباقة الأساسية',
'name_en' => 'Basic Plan',
'max_companies' => 3,
'max_invoices_month' => 100,
'max_users' => 3,
'price_jod' => 15.00,
'max_invoices_month' => 500,
'max_users' => 2,
'price_jod' => 15.00, // Default legacy price
'price_monthly_jod' => 15.00,
'price_annual_jod' => 120.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'blue',
'description_ar' => 'للمحاسبين المستقلين — 3 شركات',
'description_ar' => 'للمحاسبين المستقلين والشركات الصغيرة — 3 شركات',
'features' => [
'استخراج الفواتير بالذكاء الاصطناعي',
'الربط المباشر مع جوفوترة',
'حتى 3 شركات',
'100 فاتورة شهرياً',
'3 مستخدمين',
'تقارير شهرية',
],
],
'office' => [
'id' => 'office',
'name_ar' => 'مكتبية',
'name_en' => 'Office',
'max_companies' => 10,
'max_invoices_month' => 500,
'max_users' => 10,
'price_jod' => 45.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'teal',
'is_popular' => true,
'description_ar' => 'للمكاتب المحاسبية — ربط مباشر بجوفوترة',
'features' => [
'كل ميزات الأساسية',
'ربط مباشر بنظام JoFotara',
'حتى 10 شركات',
'500 فاتورة شهرياً',
'10 مستخدمين',
'تقارير متقدمة + تصدير',
'دعم فني بالأولوية',
'حتى 3 شركات (بدلاً من واحدة)',
'500 فاتورة شهرياً (سخية جداً)',
'مستخدمين اثنين',
'دعم فني عبر الواتساب',
],
],
'pro' => [
'id' => 'pro',
'name_ar' => 'احترافية',
'name_en' => 'Pro',
'max_companies' => 25,
'max_invoices_month' => 2000,
'max_users' => 25,
'price_jod' => 99.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'navy',
'description_ar' => 'للمكاتب الكبيرة — حجم عمل ضخم بلا حدود عملية',
'features' => [
'كل ميزات المكتبية',
'حتى 25 شركة',
'2000 فاتورة شهرياً',
'25 مستخدم',
'API كامل لتطبيق الهاتف',
'تدقيق ذكي بالـ AI (Pre-Audit)',
'مدير حساب مخصص',
],
],
'enterprise' => [
'id' => 'enterprise',
'name_ar' => 'مؤسسية',
'name_en' => 'Enterprise',
'max_companies' => 999,
'max_invoices_month' => 99999,
'max_users' => 999,
'price_jod' => 249.00,
'name_ar' => 'الباقة الاحترافية',
'name_en' => 'Pro Plan',
'max_companies' => 9999,
'max_invoices_month' => 3000,
'max_users' => 5,
'price_jod' => 35.00, // Default legacy price
'price_monthly_jod' => 35.00,
'price_annual_jod' => 290.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'gold',
'description_ar' => 'للمؤسسات — بلا حدود مع دعم مخصص',
'is_popular' => true,
'description_ar' => 'للمكاتب الكبيرة والموزعين — حجم عمل ضخم',
'features' => [
'كل ميزات الاحترافية',
'شركات وفواتير بلا حدود عملية',
'مستخدمين بلا حدود',
'SLA مضمون 99.9%',
'ربط API مخصص',
'تدريب فريق المحاسبة',
'نسخ احتياطي مخصص',
'استخراج الفواتير بالذكاء الاصطناعي',
'الربط المباشر مع جوفوترة',
'عدد شركات غير محدود',
'3,000 فاتورة شهرياً',
'5 مستخدمين',
'API كامل لتطبيق الهاتف',
'مدير حساب مخصص',
],
],
];

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info
$stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ?
@@ -57,10 +57,12 @@ final class QuotaMiddleware
json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403);
}
// Auto-reset monthly counter if billing period has ended
// Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+30 days'));
$cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare("
UPDATE subscriptions
@@ -76,15 +78,15 @@ final class QuotaMiddleware
$sub['current_period_start'] = $newStart;
$sub['current_period_end'] = $newEnd;
error_log("QuotaMiddleware: Auto-reset monthly counter for tenant {$tenantId}");
error_log("QuotaMiddleware: Auto-reset annual counter for tenant {$tenantId}");
}
// Check invoice quota
$used = (int)$sub['invoices_used_this_month'];
$limit = (int)$sub['max_invoices_per_month'];
$limit = (int)$sub['max_invoices_per_month']; // Keeping the DB column name the same for compatibility
if ($used >= $limit) {
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
'quota_type' => 'invoices',
'used' => $used,
'limit' => $limit,

View File

@@ -39,8 +39,71 @@ if (!$user || !password_verify($password, $user['password_hash'])) {
json_error('بيانات الدخول غير صحيحة', 401);
}
// 3. Handle device registration if provided (for mobile app login)
$deviceId = $data['device_id'] ?? null;
$isReviewer = (strtolower($email) === 'reviewer@musadaq.jo');
if ($deviceId && !$isReviewer) {
// Generate and send WhatsApp OTP
$phone = $user['phone'] ? (\App\Core\Encryption::decrypt($user['phone']) ?: $user['phone']) : null;
if (empty($phone)) {
json_error('رقم الهاتف غير مسجل لهذا المستخدم. يرجى التواصل مع المسؤول.', 403);
}
$phone = preg_replace('/[^0-9+]/', '', $phone);
$phone = ltrim($phone, '+');
if (str_starts_with($phone, '07')) {
$phone = '962' . substr($phone, 1);
} elseif (str_starts_with($phone, '7')) {
$phone = '962' . $phone;
}
$otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT);
$otpHash = password_hash($otp, PASSWORD_DEFAULT);
$phoneHash = hash('sha256', $phone);
$cacheDir = STORAGE_PATH . '/cache/otp';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$otpData = [
'hash' => $otpHash,
'user_id' => $user['id'],
'attempts' => 0,
'max_attempts' => 5,
'expires_at' => time() + 300,
'created_at' => time(),
];
$fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w');
if ($fp) {
flock($fp, LOCK_EX);
fwrite($fp, json_encode($otpData));
flock($fp, LOCK_UN);
fclose($fp);
}
$whatsappService = new \App\Services\WhatsAppProxyService();
$message = "رمز التحقق لتطبيق مُصادَق:\n*{$otp}*\n\nصالح لمدة 5 دقائق.";
$result = $whatsappService->sendMessage($phone, $message);
if (!$result['success']) {
error_log("ERROR: Failed to send OTP WhatsApp to phone: {$phone}");
json_error('عذراً، فشل في إرسال رمز التحقق. يرجى المحاولة مرة أخرى.', 500);
}
if (env('APP_DEBUG', 'false') === 'true') {
error_log("DEV OTP for {$phone}: {$otp}");
}
json_success([
'otp_required' => true,
'phone' => $phone,
], 'تم إرسال رمز التحقق إلى رقم هاتفك المسجل عبر واتساب');
exit;
}
// 3. Handle device registration if provided (for mobile app login)
$deviceName = $data['device_name'] ?? 'Web Browser';
$deviceSecret = null;

View File

@@ -32,8 +32,13 @@ if ($errors) {
$db = Database::getInstance();
$tenantId = $decoded['tenant_id'];
$userId = $decoded['user_id'];
$planId = $data['plan_id'];
$userId = $decoded['user_id'];
$planId = $data['plan_id'];
$cycle = $data['billing_cycle'] ?? 'annual'; // Default to annual
if (!in_array($cycle, ['monthly', 'annual'])) {
json_error('دورة الفوترة غير صالحة.', 422);
}
try {
// 1. Get plan details
@@ -45,6 +50,9 @@ try {
json_error('الباقة المختارة غير صالحة أو غير نشطة.', 422);
}
// Determine amount based on cycle
$amount = ($cycle === 'monthly') ? ($plan['price_monthly_jod'] ?? $plan['price_jod']) : ($plan['price_annual_jod'] ?? ($plan['price_jod'] * 10));
// 2. Check for existing pending payment for this tenant
$stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1");
$stmt->execute([$tenantId]);
@@ -68,15 +76,16 @@ try {
// 6. Create payment request
$paymentId = Database::generateUuid();
$stmt = $db->prepare("
INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())
INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, billing_cycle, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())
");
$stmt->execute([
$paymentId,
$tenantId,
$userId,
$planId,
$plan['price_jod'],
$cycle,
$amount,
$referenceNumber,
$cliqAlias,
$user['name'] ?? ''
@@ -88,17 +97,17 @@ try {
$tenantId,
$userId,
$paymentId,
json_encode(['plan_id' => $planId, 'amount' => $plan['price_jod'], 'ref' => $referenceNumber])
json_encode(['plan_id' => $planId, 'cycle' => $cycle, 'amount' => $amount, 'ref' => $referenceNumber])
]);
json_success([
'payment_id' => $paymentId,
'reference_number' => $referenceNumber,
'cliq_alias' => $cliqAlias,
'amount_jod' => (float)$plan['price_jod'],
'plan_name' => $plan['name_ar'] ?? $plan['name_en'],
'amount_jod' => (float)$amount,
'plan_name' => ($plan['name_ar'] ?? $plan['name_en']) . " (" . ($cycle === 'monthly' ? 'شهري' : 'سنوي') . ")",
'payer_name' => $user['name'] ?? '',
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$plan['price_jod']} دينار أردني.",
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$amount} دينار أردني.",
], 'تم إنشاء طلب الدفع بنجاح');
} catch (\Throwable $e) {

View File

@@ -47,18 +47,33 @@ try {
$plan = $stmt->fetch();
if ($plan) {
$cycle = $payment['billing_cycle'] ?? 'annual';
$startDate = date('Y-m-d H:i:s');
$endDate = date('Y-m-d H:i:s', strtotime('+30 days'));
if ($cycle === 'monthly') {
$endDate = date('Y-m-d H:i:s', strtotime('+30 days'));
$maxInvoices = (int)$plan['max_invoices_month'];
$price = (float)($plan['price_monthly_jod'] ?? $plan['price_jod']);
} else {
$endDate = date('Y-m-d H:i:s', strtotime('+1 year'));
// Annual gets 12x the monthly quota
$maxInvoices = (int)($plan['max_invoices_month'] * 12);
$price = (float)($plan['price_annual_jod'] ?? ($plan['price_jod'] * 10));
}
$stmt = $db->prepare("
INSERT INTO subscriptions (tenant_id, plan_id, max_companies, max_invoices_per_month, max_users, price_jod, status, current_period_start, current_period_end, updated_at)
VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, 'active', :start, :end, NOW())
INSERT INTO subscriptions (
tenant_id, plan_id, max_companies, max_invoices_per_month, max_users,
price_jod, billing_cycle, status, current_period_start, current_period_end, updated_at
)
VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, :cycle, 'active', :start, :end, NOW())
ON DUPLICATE KEY UPDATE
plan_id = VALUES(plan_id),
max_companies = VALUES(max_companies),
max_invoices_per_month = VALUES(max_invoices_per_month),
max_users = VALUES(max_users),
price_jod = VALUES(price_jod),
billing_cycle = VALUES(billing_cycle),
status = 'active',
current_period_start = VALUES(current_period_start),
current_period_end = VALUES(current_period_end),
@@ -68,9 +83,10 @@ try {
't_id' => $payment['tenant_id'],
'p_id' => $plan['id'],
'max_c' => $plan['max_companies'],
'max_i' => $plan['max_invoices_month'],
'max_i' => $maxInvoices,
'max_u' => $plan['max_users'],
'price' => $plan['price_jod'],
'price' => $price,
'cycle' => $cycle,
'start' => $startDate,
'end' => $endDate
]);

View File

@@ -13,7 +13,7 @@ $db = Database::getInstance();
try {
$stmt = $db->query("
SELECT id, name_ar, name_en, max_companies, max_invoices_month, max_users,
price_jod, ai_features, jofotara_enabled, sort_order
price_jod, price_annual_jod, price_monthly_jod, ai_features, jofotara_enabled, sort_order
FROM subscription_plans
WHERE is_active = 1
ORDER BY sort_order ASC
@@ -36,6 +36,8 @@ try {
$plan['max_invoices_month'] = (int)$plan['max_invoices_month'];
$plan['max_users'] = (int)$plan['max_users'];
$plan['price_jod'] = (float)$plan['price_jod'];
$plan['price_annual_jod'] = (float)$plan['price_annual_jod'];
$plan['price_monthly_jod'] = (float)$plan['price_monthly_jod'];
$plan['ai_features'] = (bool)$plan['ai_features'];
$plan['jofotara_enabled'] = (bool)$plan['jofotara_enabled'];
}

View File

@@ -18,8 +18,8 @@ $data = input();
$errors = Validator::validate($data, [
'name' => 'required',
'email' => 'required|email',
'phone' => 'required',
'manager_name' => 'required',
'manager_email' => 'required|email',
'manager_password' => 'required'
]);
@@ -43,12 +43,23 @@ try {
$encryptedTenantName = \App\Core\Encryption::encrypt($data['name']);
$encryptedTenantEmail = \App\Core\Encryption::encrypt($data['email']);
$phone = preg_replace('/[^0-9+]/', '', $data['phone']);
$phone = ltrim($phone, '+');
if (str_starts_with($phone, '07')) {
$phone = '962' . substr($phone, 1);
} elseif (str_starts_with($phone, '7')) {
$phone = '962' . $phone;
}
$encryptedPhone = \App\Core\Encryption::encrypt($phone);
$phoneHash = hash('sha256', $phone);
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())");
$stmt->execute([
$tenantId,
$encryptedTenantName,
$encryptedTenantEmail,
$data['phone'] ?? null
$phone
]);
// Generate User UUID
@@ -60,17 +71,19 @@ try {
// Encrypt sensitive user data
$encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
$encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']);
$emailHash = hash('sha256', strtolower($data['manager_email']));
$encryptedEmail = \App\Core\Encryption::encrypt($data['email']);
$emailHash = hash('sha256', strtolower($data['email']));
// 2. Create Initial Manager (Admin) for this Tenant
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())");
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, phone, phone_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'admin', NOW())");
$stmtUser->execute([
$userId,
$tenantId,
$encryptedName,
$encryptedEmail,
$emailHash,
$encryptedPhone,
$phoneHash,
password_hash($data['manager_password'], PASSWORD_DEFAULT)
]);

View File

@@ -6,7 +6,6 @@
<!-- Camera & Storage (Invoice Scanning & Picking) -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Audio (Voice Assistant) -->

View File

@@ -12,7 +12,7 @@ import '../../../core/services/push_notification_service.dart';
class AuthController extends GetxController {
final Dio _dio = DioClient().client;
final SecureStorage _storage = SecureStorage();
var isLoading = false.obs;
var phone = ''.obs;
@@ -23,20 +23,20 @@ class AuthController extends GetxController {
return;
}
isLoading.value = true;
// Normalize phone number
String normalizedPhone = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
if (normalizedPhone.startsWith('+')) {
normalizedPhone = normalizedPhone.substring(1);
}
if (normalizedPhone.startsWith('07')) {
normalizedPhone = '962' + normalizedPhone.substring(1);
normalizedPhone = '962${normalizedPhone.substring(1)}';
} else if (normalizedPhone.startsWith('7')) {
normalizedPhone = '962' + normalizedPhone;
normalizedPhone = '962$normalizedPhone';
}
phone.value = normalizedPhone;
final response = await _dio.post('auth/mobile/request-otp', data: {
'phone': normalizedPhone,
});
@@ -60,12 +60,12 @@ class AuthController extends GetxController {
Future<void> verifyOtp(String otp) async {
try {
isLoading.value = true;
// Get device info
final deviceInfo = DeviceInfoPlugin();
String deviceId = '';
String deviceName = '';
if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
deviceId = androidInfo.id;
@@ -92,16 +92,16 @@ class AuthController extends GetxController {
if (response.statusCode == 200) {
AppLogger.print('OTP Verify Success. Tokens received.');
final data = response.data['data'];
// Save secure data
await _storage.saveToken(data['access_token']);
await _storage.saveDeviceSecret(data['device_secret']);
if (data['user']['email'] != null) {
await _storage.saveEmail(data['user']['email']);
}
AppSnackbar.showSuccess('مرحباً بك', 'تم تسجيل الدخول بنجاح');
// Navigate to Biometric Setup (unless it's the reviewer)
if (data['user']['email'] == 'reviewer@musadaq.jo') {
Get.offAllNamed(AppRoutes.MAIN);
@@ -111,7 +111,8 @@ class AuthController extends GetxController {
}
} on DioException catch (e, stackTrace) {
AppLogger.error('OTP Verify Failed', e.response?.data, stackTrace);
AppSnackbar.showError('خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
AppSnackbar.showError(
'خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
} finally {
isLoading.value = false;
}
@@ -120,7 +121,8 @@ class AuthController extends GetxController {
Future<void> loginWithEmail(String email, String password) async {
try {
if (email.trim().isEmpty || password.trim().isEmpty) {
AppSnackbar.showError('خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور');
AppSnackbar.showError(
'خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور');
return;
}
isLoading.value = true;
@@ -129,7 +131,7 @@ class AuthController extends GetxController {
final deviceInfo = DeviceInfoPlugin();
String deviceId = '';
String deviceName = '';
if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
deviceId = androidInfo.id;
@@ -150,22 +152,31 @@ class AuthController extends GetxController {
});
if (response.statusCode == 200) {
AppLogger.print('Email Login Success. Tokens received.');
final data = response.data['data'];
if (data['otp_required'] == true) {
AppLogger.print('Email Login verification required via OTP.');
phone.value = data['phone'] ?? '';
AppSnackbar.showSuccess('نجاح', 'تم إرسال رمز التحقق إلى رقم هاتفك المسجل');
Get.toNamed(AppRoutes.OTP_VERIFY);
return;
}
AppLogger.print('Email Login Success. Tokens received.');
// Save secure data
await _storage.saveToken(data['access_token']);
// Note: auth/login might not return device_secret, handle if missing
if (data['device_secret'] != null) {
await _storage.saveDeviceSecret(data['device_secret']);
}
if (data['user']['email'] != null) {
await _storage.saveEmail(data['user']['email']);
}
AppSnackbar.showSuccess('مرحباً بك', 'تم تسجيل الدخول بنجاح');
// Navigate to Dashboard for reviewer, else Biometric Setup
if (email == 'reviewer@musadaq.jo') {
Get.offAllNamed(AppRoutes.MAIN);

View File

@@ -6,7 +6,7 @@ class PhoneInputView extends StatelessWidget {
PhoneInputView({super.key});
final AuthController controller = Get.put(AuthController());
final TextEditingController phoneController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
@override
@@ -37,47 +37,38 @@ class PhoneInputView extends StatelessWidget {
),
const SizedBox(height: 8),
const Text(
'أدخل رقم هاتفك أو البريد الإلكتروني لتسجيل الدخول',
'أدخل البريد الإلكتروني وكلمة المرور لتسجيل الدخول',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
TextField(
controller: phoneController,
controller: emailController,
keyboardType: TextInputType.emailAddress,
textDirection: TextDirection.ltr,
onChanged: (val) => controller.phone.value = val,
decoration: InputDecoration(
labelText: 'رقم الهاتف أو البريد الإلكتروني',
prefixIcon: const Icon(Icons.person_outline),
labelText: 'البريد الإلكتروني',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16),
Obx(() {
final isEmail = controller.phone.value.contains('@');
if (!isEmail) return const SizedBox.shrink();
return Column(
children: [
TextField(
controller: passwordController,
obscureText: true,
textDirection: TextDirection.ltr,
decoration: InputDecoration(
labelText: 'كلمة المرور',
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 24),
],
);
}),
TextField(
controller: passwordController,
obscureText: true,
textDirection: TextDirection.ltr,
decoration: InputDecoration(
labelText: 'كلمة المرور',
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 24),
Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -88,22 +79,16 @@ class PhoneInputView extends StatelessWidget {
onPressed: controller.isLoading.value
? null
: () {
if (controller.phone.value.contains('@')) {
controller.loginWithEmail(
controller.phone.value,
passwordController.text
);
} else {
controller.requestOtp(phoneController.text);
}
controller.loginWithEmail(
emailController.text,
passwordController.text
);
},
child: controller.isLoading.value
? const CircularProgressIndicator(color: Colors.white)
: Text(
controller.phone.value.contains('@')
? 'تسجيل الدخول'
: 'إرسال رمز التحقق',
style: const TextStyle(fontSize: 16)
: const Text(
'تسجيل الدخول',
style: TextStyle(fontSize: 16)
),
)),
],

View File

@@ -254,14 +254,16 @@ class DashboardView extends GetView<DashboardController> {
isDark,
() => Get.toNamed(AppRoutes.AUDIT_LOG),
),
const SizedBox(width: 12),
_buildAdminActionCard(
'ادعُ واكسب',
Icons.card_giftcard,
const Color(0xFFD4AF37),
isDark,
() => Get.toNamed(AppRoutes.REFERRAL),
),
if (!GetPlatform.isIOS) ...[
const SizedBox(width: 12),
_buildAdminActionCard(
'ادعُ واكسب',
Icons.card_giftcard,
const Color(0xFFD4AF37),
isDark,
() => Get.toNamed(AppRoutes.REFERRAL),
),
],
const SizedBox(width: 12),
_buildAdminActionCard(
'استهلاك AI',

View File

@@ -5,7 +5,7 @@ import '../../../app/routes/app_pages.dart';
class OnboardingController extends GetxController {
var currentPage = 0.obs;
final List<OnboardingModel> items = [
List<OnboardingModel> get items => [
OnboardingModel(
title: 'مرحباً بك في مُصادَق',
description:
@@ -19,9 +19,10 @@ class OnboardingController extends GetxController {
imageAsset: 'assets/images/onboarding_2.png',
),
OnboardingModel(
title: 'مدفوعات فورية آمنة',
description:
'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.',
title: 'إدارة متكاملة لشركتك',
description: GetPlatform.isIOS
? 'إدارة فواتير الشركات وأرشفة ضريبية متكاملة بأمان وسهولة تامة.'
: 'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.',
imageAsset: 'assets/images/onboarding_3.png',
),
];

View File

@@ -166,7 +166,7 @@ class SettingsView extends GetView<SettingsController> {
_buildInfoTile(
icon: Icons.diamond_rounded,
title: 'الاشتراكات والباقات',
trailing: 'ترقية →',
trailing: GetPlatform.isIOS ? 'التفاصيل →' : 'ترقية →',
isDark: isDark,
onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION),
),

View File

@@ -11,6 +11,7 @@ class SubscriptionController extends GetxController {
var isLoading = true.obs;
var isCreatingPayment = false.obs;
var activePaymentRequest = Rxn<Map<String, dynamic>>();
var isAnnual = true.obs; // Toggle between Monthly and Annual
@override
void onInit() {
@@ -69,7 +70,11 @@ class SubscriptionController extends GetxController {
Future<Map<String, dynamic>?> createPaymentRequest(String planId) async {
try {
isCreatingPayment.value = true;
final res = await DioClient().client.post('payments/create', data: {'plan_id': planId});
final cycle = isAnnual.value ? 'annual' : 'monthly';
final res = await DioClient().client.post('payments/create', data: {
'plan_id': planId,
'billing_cycle': cycle,
});
if (res.data['success'] == true && res.data['data'] != null) {
final result = Map<String, dynamic>.from(res.data['data']);
activePaymentRequest.value = result;

View File

@@ -34,46 +34,122 @@ class SubscriptionView extends StatelessWidget {
children: [
// Current Subscription Status
if (controller.currentSubscription.value != null)
_buildCurrentPlan(controller.currentSubscription.value!, isDark),
_buildCurrentPlan(controller.currentSubscription.value!, isDark)
else
_buildFreePlanPlaceholder(isDark),
const SizedBox(height: 24),
// Active Payment Request Banner
if (controller.activePaymentRequest.value != null)
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
if (GetPlatform.isIOS) ...[
_buildIOSB2BInfoCard(isDark),
],
// Plans Header
Row(
children: [
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22),
const SizedBox(width: 8),
Text(
'اختر باقتك',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : const Color(0xFF0F172A),
if (!GetPlatform.isIOS) ...[
// Active Payment Request Banner
if (controller.activePaymentRequest.value != null)
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
// Plans Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22),
const SizedBox(width: 8),
Text(
'اختر باقتك',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
),
],
),
const SizedBox(height: 4),
Text(
'ادفع عبر CliQ — بدون عمولة!',
style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey),
),
],
),
],
),
const SizedBox(height: 20),
// Toggle
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => controller.isAnnual.value = false,
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: !controller.isAnnual.value ? const Color(0xFF0F4C81) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'دفع شهري',
style: TextStyle(
color: !controller.isAnnual.value ? Colors.white : (isDark ? Colors.white60 : Colors.black54),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
)),
),
),
Expanded(
child: GestureDetector(
onTap: () => controller.isAnnual.value = true,
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: controller.isAnnual.value ? const Color(0xFF0F4C81) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'دفع سنوي (توفير ✨)',
style: TextStyle(
color: controller.isAnnual.value ? Colors.white : (isDark ? Colors.white60 : Colors.black54),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
)),
),
),
],
),
),
const SizedBox(height: 20),
// Plans Grid
...controller.plans.map((plan) => _buildPlanCard(plan, controller, isDark)),
const SizedBox(height: 24),
// Payment History
if (controller.myPayments.isNotEmpty) ...[
const Text('سجل المدفوعات', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...controller.myPayments.map((p) => _buildPaymentHistoryItem(p, isDark)),
],
),
const SizedBox(height: 4),
Text(
'ادفع عبر CliQ — بدون عمولة!',
style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey),
),
const SizedBox(height: 16),
// Plans Grid
...controller.plans.map((plan) => _buildPlanCard(plan, controller, isDark)),
const SizedBox(height: 24),
// Payment History
if (controller.myPayments.isNotEmpty) ...[
const Text('سجل المدفوعات', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...controller.myPayments.map((p) => _buildPaymentHistoryItem(p, isDark)),
],
const SizedBox(height: 40),
@@ -85,6 +161,84 @@ class SubscriptionView extends StatelessWidget {
);
}
Widget _buildFreePlanPlaceholder(bool isDark) {
return _buildCurrentPlan({
'plan_name': 'الباقة الافتراضية (المجانية)',
'days_remaining': 0,
'invoices': {
'used': 0,
'limit': 100,
}
}, isDark);
}
Widget _buildIOSB2BInfoCard(bool isDark) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark ? Colors.white10 : Colors.grey.shade200,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.business_rounded, color: const Color(0xFF0F4C81), size: 24),
const SizedBox(width: 8),
Text(
'الاشتراكات المؤسسية (B2B)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
),
],
),
const SizedBox(height: 12),
Text(
'تطبيق "مُصادَق" مخصص لإدارة الفواتير الضريبية للشركات ومكاتب المحاسبة. يتم تفعيل وإدارة باقات الاشتراك والميزات الإضافية مركزياً عن طريق لوحة التحكم الخاصة بالمسؤول في منشأتك.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: isDark ? Colors.white70 : Colors.black87,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F4C81).withOpacity(0.05),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Icons.info_outline, color: const Color(0xFF0F4C81), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'إذا كنت بحاجة إلى ترقية باقتك أو زيادة الحصص (Quotas)، يرجى التواصل مع مسؤول تكنولوجيا المعلومات أو المحاسب المسؤول في شركتك.',
style: TextStyle(
fontSize: 12,
height: 1.4,
color: const Color(0xFF0F4C81),
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
);
}
Widget _buildCurrentPlan(Map<String, dynamic> sub, bool isDark) {
final planName = sub['plan_name'] ?? sub['plan_name_en'] ?? sub['plan_id'] ?? 'مجانية';
final daysLeft = sub['days_remaining'] ?? 0;
@@ -228,9 +382,19 @@ class SubscriptionView extends StatelessWidget {
Widget _buildPlanCard(Map<String, dynamic> plan, SubscriptionController ctrl, bool isDark) {
final isPopular = plan['is_popular'] == true;
final price = (plan['price_jod'] ?? 0).toString();
final features = (plan['features'] as List?)?.cast<String>() ?? [];
final nameAr = plan['name_ar'] ?? plan['name_en'] ?? 'باقة';
return Obx(() {
final bool annual = ctrl.isAnnual.value;
final price = annual
? (plan['price_annual_jod'] ?? (plan['price_jod'] * 10)).toString()
: (plan['price_monthly_jod'] ?? plan['price_jod']).toString();
final features = (plan['features'] as List?)?.cast<String>() ?? [];
final invoiceLimit = annual
? (plan['max_invoices_month'] * 12).toString()
: (plan['max_invoices_month']).toString();
final cycleText = annual ? 'سنة' : 'شهر';
return Container(
margin: const EdgeInsets.only(bottom: 16),
@@ -271,7 +435,7 @@ class SubscriptionView extends StatelessWidget {
text: TextSpan(
children: [
TextSpan(text: price, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81))),
TextSpan(text: ' JOD', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
TextSpan(text: ' JOD / $cycleText', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
],
),
),
@@ -302,7 +466,7 @@ class SubscriptionView extends StatelessWidget {
children: [
_buildPlanStat(Icons.business, '${plan['max_companies'] ?? 0} شركات'),
const SizedBox(width: 8),
_buildPlanStat(Icons.receipt_long, '${plan['max_invoices_month'] ?? 0} فاتورة/شهر'),
_buildPlanStat(Icons.receipt_long, '$invoiceLimit فاتورة/$cycleText'),
const SizedBox(width: 8),
_buildPlanStat(Icons.people, '${plan['max_users'] ?? 0} مستخدمين'),
],
@@ -338,6 +502,7 @@ class SubscriptionView extends StatelessWidget {
],
),
);
});
}
Widget _buildPlanStat(IconData icon, String text) {

View File

@@ -9,8 +9,8 @@ import 'tenants_management_controller.dart';
class AddTenantController extends GetxController {
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final managerNameController = TextEditingController();
final managerEmailController = TextEditingController();
final managerPasswordController = TextEditingController();
var isSubmitting = false.obs;
@@ -20,8 +20,8 @@ class AddTenantController extends GetxController {
void onClose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
managerNameController.dispose();
managerEmailController.dispose();
managerPasswordController.dispose();
super.onClose();
}
@@ -29,11 +29,11 @@ class AddTenantController extends GetxController {
Future<void> submit() async {
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
final managerName = managerNameController.text.trim();
final managerEmail = managerEmailController.text.trim();
final managerPassword = managerPasswordController.text;
if (name.isEmpty || email.isEmpty || managerName.isEmpty || managerEmail.isEmpty || managerPassword.isEmpty) {
if (name.isEmpty || email.isEmpty || phone.isEmpty || managerName.isEmpty || managerPassword.isEmpty) {
AppSnackbar.showWarning('تنبيه', 'الرجاء إدخال جميع البيانات المطلوبة');
return;
}
@@ -43,8 +43,8 @@ class AddTenantController extends GetxController {
final response = await _dio.post('tenants/create', data: {
'name': name,
'email': email,
'phone': phone,
'manager_name': managerName,
'manager_email': managerEmail,
'manager_password': managerPassword,
});

View File

@@ -36,29 +36,29 @@ class AddTenantView extends StatelessWidget {
const SizedBox(height: 16),
_buildTextField(
controller: controller.emailController,
label: 'البريد الإلكتروني للمكتب',
label: 'البريد الإلكتروني للعمل',
icon: Icons.email,
keyboardType: TextInputType.emailAddress,
isDark: isDark,
),
const SizedBox(height: 24),
const Text(
'بيانات مدير المكتب',
'بيانات مدير المكتب المسؤول',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.managerNameController,
label: 'اسم المدير',
label: 'اسم المدير الكامل',
icon: Icons.person,
isDark: isDark,
),
const SizedBox(height: 16),
_buildTextField(
controller: controller.managerEmailController,
label: 'البريد الإلكتروني للمدير',
icon: Icons.alternate_email,
keyboardType: TextInputType.emailAddress,
controller: controller.phoneController,
label: 'رقم هاتف المدير (لتسجيل الدخول OTP)',
icon: Icons.phone,
keyboardType: TextInputType.phone,
isDark: isDark,
),
const SizedBox(height: 16),

View File

@@ -5,26 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev"
source: hosted
version: "88.0.0"
version: "93.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad
sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
url: "https://pub.dev"
source: hosted
version: "1.3.69"
version: "1.3.71"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev"
source: hosted
version: "8.1.1"
version: "10.0.1"
archive:
dependency: transitive
description:
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.13.1"
barcode:
dependency: transitive
description:
@@ -77,18 +77,18 @@ packages:
dependency: transitive
description:
name: build
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "4.0.6"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
build_daemon:
dependency: transitive
description:
@@ -97,30 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
url: "https://pub.dev"
source: hosted
version: "3.0.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
url: "https://pub.dev"
source: hosted
version: "9.3.1"
version: "2.15.0"
built_collection:
dependency: transitive
description:
@@ -217,14 +201,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_builder:
code_assets:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
version: "1.0.0"
collection:
dependency: transitive
description:
@@ -293,18 +277,18 @@ packages:
dependency: "direct main"
description:
name: cunning_document_scanner
sha256: bf590e8c8c8a4903ba7873c4f22b67e976604853f11065f594cb19b408bd25ef
sha256: de0c0705799f7d5cc9b82b67bfb8b3e965a1fbff4afbd70ea10cd1dad4f3a98c
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.4.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.7"
dbus:
dependency: transitive
description:
@@ -405,10 +389,10 @@ packages:
dependency: transitive
description:
name: file_selector_macos
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.4+4"
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
@@ -429,50 +413,50 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158
sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
version: "4.9.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
version: "7.0.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c
sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
version: "3.7.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a
sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
url: "https://pub.dev"
source: hosted
version: "16.2.0"
version: "16.2.2"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a"
sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
url: "https://pub.dev"
source: hosted
version: "4.7.9"
version: "4.7.11"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1"
sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
url: "https://pub.dev"
source: hosted
version: "4.1.5"
version: "4.1.7"
fixnum:
dependency: transitive
description:
@@ -485,10 +469,10 @@ packages:
dependency: transitive
description:
name: flat_buffers
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3"
sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345"
url: "https://pub.dev"
source: hosted
version: "23.5.26"
version: "25.9.23"
flutter:
dependency: "direct main"
description: flutter
@@ -570,26 +554,26 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.31"
version: "2.0.34"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87"
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
url: "https://pub.dev"
source: hosted
version: "10.1.0"
version: "10.2.0"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "3af15a3cb2bf5b8b776832bd01776f8018766aece55623176e28b406481fb320"
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.3.1"
flutter_secure_storage_linux:
dependency: transitive
description:
@@ -636,18 +620,10 @@ packages:
dependency: "direct main"
description:
name: freerasp
sha256: "76a3fb6f8e3fdd7d83e224866998523e7fb79d5779321983e484a6cfbf4b01b5"
sha256: "5b9a3402a7a30d928897e2264e2700a30db1df14de289f500b3a0cf50dc19df2"
url: "https://pub.dev"
source: hosted
version: "6.12.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "7.5.1"
get:
dependency: "direct main"
description:
@@ -672,6 +648,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
html:
dependency: transitive
description:
@@ -708,26 +692,26 @@ packages:
dependency: "direct main"
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.5.4"
version: "4.8.0"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev"
source: hosted
version: "0.8.13+1"
version: "0.8.13+17"
image_picker_for_web:
dependency: transitive
description:
@@ -740,10 +724,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13"
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
@@ -756,10 +740,10 @@ packages:
dependency: transitive
description:
name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
@@ -792,14 +776,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
@@ -844,18 +844,18 @@ packages:
dependency: transitive
description:
name: local_auth_android
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
url: "https://pub.dev"
source: hosted
version: "1.0.52"
version: "1.0.56"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
version: "1.6.1"
local_auth_platform_interface:
dependency: transitive
description:
@@ -884,10 +884,10 @@ packages:
dependency: "direct main"
description:
name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.3.3"
matcher:
dependency: transitive
description:
@@ -924,10 +924,18 @@ packages:
dependency: transitive
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nm:
dependency: transitive
description:
@@ -940,26 +948,34 @@ packages:
dependency: "direct main"
description:
name: objectbox
sha256: "3cc186749178a3556e1020c9082d0897d0f9ecbdefcc27320e65c5bc650f0e57"
sha256: "83d58e0ab5c4180a2f67086c449a4f2d1475932b31819271923dfc82a76f73c6"
url: "https://pub.dev"
source: hosted
version: "4.3.1"
version: "5.3.1"
objectbox_flutter_libs:
dependency: "direct main"
description:
name: objectbox_flutter_libs
sha256: cd754766e04229a4f51250f121813d9a3c1a74fc21cd68e48b3c6085cbcd6c85
sha256: fd4e8ed03e2b2bfeb8aa965435d7c5523ec7e962f1ffecf6e7da6c1f4d170419
url: "https://pub.dev"
source: hosted
version: "4.3.1"
version: "5.3.1"
objectbox_generator:
dependency: "direct dev"
description:
name: objectbox_generator
sha256: "71a3f6948e631be5c7160d512ad2a8cb7471cdbcf1731ec6baf2a794b82386d7"
sha256: daa95f21c7140c619ffc1abee2465541d4f0317b8c3bbf2141be1a9e5c507a1d
url: "https://pub.dev"
source: hosted
version: "4.3.1"
version: "5.3.1"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
octo_image:
dependency: transitive
description:
@@ -1020,18 +1036,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.2.19"
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -1060,10 +1076,10 @@ packages:
dependency: "direct main"
description:
name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
url: "https://pub.dev"
source: hosted
version: "3.11.3"
version: "3.12.0"
pdf_widget_wrapper:
dependency: transitive
description:
@@ -1072,30 +1088,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
pedantic:
dependency: transitive
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "11.4.0"
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
@@ -1256,6 +1264,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
record_web:
dependency: transitive
description:
@@ -1337,66 +1353,66 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "4.2.3"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
speech_to_text:
dependency: "direct main"
description:
name: speech_to_text
sha256: c07557664974afa061f221d0d4186935bea4220728ea9446702825e8b988db04
sha256: "75587f7400f485fdf166beacd471549d98fe5d58e634f708916bb65dec05d6a4"
url: "https://pub.dev"
source: hosted
version: "7.3.0"
version: "7.4.0"
speech_to_text_platform_interface:
dependency: transitive
description:
name: speech_to_text_platform_interface
sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114
sha256: a7e16e02853853ed7534ac2bde9a1c4f39c8879970a7974ac6ff832d4bdaa4b0
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.4.0"
speech_to_text_windows:
dependency: transitive
description:
name: speech_to_text_windows
sha256: "2c9846d18253c7bbe059a276297ef9f27e8a2745dead32192525beb208195072"
sha256: "2d1d10565b23262386b453b33656299608dc7a66784453735d6c1318f13f44d7"
url: "https://pub.dev"
source: hosted
version: "1.0.0+beta.8"
version: "1.0.1"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.2+1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
version: "2.5.8"
sqflite_darwin:
dependency: transitive
description:
@@ -1449,10 +1465,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.0+1"
term_glyph:
dependency: transitive
description:
@@ -1469,14 +1485,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.9"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
@@ -1497,18 +1505,18 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.dev"
source: hosted
version: "6.3.20"
version: "6.3.29"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.3.4"
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
@@ -1521,10 +1529,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1537,10 +1545,10 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
@@ -1569,10 +1577,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.0.0"
version: "15.2.0"
watcher:
dependency: transitive
description:
@@ -1646,5 +1654,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.32.0"
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -1,7 +1,7 @@
name: musadaq_app
description: Jordanian E-Invoicing Automation SaaS
publish_to: 'none'
version: 1.0.3+3
version: 1.0.6+6
environment:
sdk: '>=3.2.0 <4.0.0'
@@ -18,8 +18,8 @@ dependencies:
flutter_secure_storage: ^10.1.0
# ─── Local Database (ObjectBox) ─────────────────────
objectbox: ^4.0.1
objectbox_flutter_libs: any
objectbox: ^5.3.1
objectbox_flutter_libs: ^5.3.1
path_provider: ^2.1.2
# ─── Authentication & Security ──────────────────────
@@ -28,8 +28,8 @@ dependencies:
crypto: ^3.0.3
# ─── Camera & Scanning ──────────────────────────────
camerawesome: ^2.0.0
cunning_document_scanner: ^1.2.3
camerawesome: ^2.5.0
cunning_document_scanner: ^1.4.0
image_picker: ^1.0.7
file_picker: ^8.1.2
@@ -46,7 +46,7 @@ dependencies:
# ─── Voice & Audio ──────────────────────────────────
speech_to_text: ^7.3.0
record: ^6.2.0
permission_handler: ^11.3.0
permission_handler: ^12.0.1
# ─── Connectivity & Background ──────────────────────
connectivity_plus: ^6.0.3
@@ -70,13 +70,13 @@ dependencies:
shorebird_code_push: ^2.0.0
# ─── Security (Root/Jailbreak/Tamper Detection) ─────
freerasp: ^6.6.0
freerasp: ^7.5.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
objectbox_generator: any
objectbox_generator: ^5.3.1
build_runner: ^2.4.8
flutter_launcher_icons: ^0.13.1

View File

@@ -164,10 +164,22 @@
<h2>اختر الباقة المناسبة لحجم أعمالك</h2>
<p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p>
</div>
<div style="display:flex; justify-content:center; margin-bottom:40px;">
<div class="cycle-toggle" style="background:rgba(255,255,255,0.05); padding:6px; border-radius:14px; display:inline-flex; border:1px solid rgba(255,255,255,0.1); cursor:pointer;" onclick="this.classList.toggle('monthly'); document.querySelectorAll('.price-monthly').forEach(el=>el.style.display=this.classList.contains('monthly')?'block':'none'); document.querySelectorAll('.price-annual').forEach(el=>el.style.display=this.classList.contains('monthly')?'none':'block');">
<span class="toggle-btn annual-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:white; background:var(--green-mid); transition:all 0.3s;">دفع سنوي (توفير )</span>
<span class="toggle-btn monthly-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:var(--text-3); transition:all 0.3s;">دفع شهري</span>
</div>
</div>
<style>
.cycle-toggle.monthly .annual-btn { background:transparent; color:var(--text-3); }
.cycle-toggle.monthly .monthly-btn { background:var(--green-mid); color:white; }
</style>
<div class="pricing-grid">
<div class="price-card">
<div class="price-name">مجانية</div>
<div class="price-name">التجربة المجانية</div>
<div class="price-amount">0 <span>دينار/شهر</span></div>
<div class="price-desc">للتجربة الأولية</div>
<ul class="price-features">
@@ -180,63 +192,38 @@
<a href="/register.php" class="btn btn-outline" style="width:100%">ابدأ مجاناً</a>
</div>
<div class="price-card">
<div class="price-name">أساسية</div>
<div class="price-amount">15 <span>دينار/شهر</span></div>
<div class="price-desc">للمحاسبين المستقلين</div>
<ul class="price-features">
<li><span class="feature-check"></span> حتى 3 شركات</li>
<li><span class="feature-check"></span> 100 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 3 مستخدمين</li>
<li><span class="feature-check"></span> تقارير شهرية</li>
<li><span class="feature-check"></span> دعم فني</li>
</ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a>
</div>
<div class="price-card popular">
<div class="popular-badge"> الأكثر اختياراً</div>
<div class="price-name">مكتبية</div>
<div class="price-amount">45 <span>دينار/شهر</span></div>
<div class="price-desc">للمكاتب المحاسبية</div>
<div class="price-name">الباقة الأساسية</div>
<div class="price-amount price-annual">120 <span>دينار/سنة</span></div>
<div class="price-amount price-monthly" style="display:none;">15 <span>دينار/شهر</span></div>
<div class="price-desc">للمحاسبين والشركات الصغيرة</div>
<ul class="price-features">
<li><span class="feature-check"></span> حتى 10 شركات</li>
<li><span class="feature-check"></span> حتى 3 شركات</li>
<li><span class="feature-check"></span> 500 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 10 مستخدمين</li>
<li><span class="feature-check"></span> تصدير Excel متقدم</li>
<li><span class="feature-check"></span> دعم بالأولوية</li>
<li><span class="feature-check"></span> مستخدمين اثنين</li>
<li><span class="feature-check"></span> دعم فني متكامل</li>
<li><span class="feature-check"></span> ربط مباشر مع جوفوترة</li>
</ul>
<a href="/register.php" class="btn btn-primary" style="width:100%">اشترك الآن</a>
</div>
<div class="price-card">
<div class="price-name">احترافية</div>
<div class="price-amount">99 <span>دينار/شهر</span></div>
<div class="price-desc">للمكاتب الكبيرة</div>
<div class="price-name">الباقة الاحترافية</div>
<div class="price-amount price-annual">290 <span>دينار/سنة</span></div>
<div class="price-amount price-monthly" style="display:none;">35 <span>دينار/شهر</span></div>
<div class="price-desc">للمكاتب الكبيرة والموزعين</div>
<ul class="price-features">
<li><span class="feature-check"></span> حتى 25 شركة</li>
<li><span class="feature-check"></span> 2,000 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 25 مستخدم</li>
<li><span class="feature-check"></span> شركات غير محدودة</li>
<li><span class="feature-check"></span> 3,000 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 5 مستخدمين</li>
<li><span class="feature-check"></span> تدقيق ذكي استباقي</li>
<li><span class="feature-check"></span> مدير حساب مخصص</li>
<li><span class="feature-check"></span> API كامل للتطبيق</li>
</ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a>
</div>
<div class="price-card">
<div class="price-name">مؤسسية</div>
<div class="price-amount">249 <span>دينار/شهر</span></div>
<div class="price-desc">للمؤسسات الكبرى</div>
<ul class="price-features">
<li><span class="feature-check"></span> شركات بلا حدود</li>
<li><span class="feature-check"></span> فواتير بلا حدود</li>
<li><span class="feature-check"></span> مستخدمين بلا حدود</li>
<li><span class="feature-check"></span> SLA مضمون 99.9%</li>
<li><span class="feature-check"></span> تدريب + نسخ احتياطي</li>
</ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">تواصل معنا</a>
</div>
</div>
</div>
</section>

View File

@@ -1868,8 +1868,8 @@
<!-- Invoices -->
<div class="usage-card">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 الفواتير
الشهرية</span>
<span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 رصيد الفواتير
<span x-text="subscription?.billing_cycle === 'monthly' ? '(شهري)' : '(سنوي)'"></span></span>
<span class="num-font" style="font-weight:700; color:var(--green-mid);"
x-text="(subscription?.invoices?.used || 0) + ' من ' + (subscription?.invoices?.limit || 0)"></span>
</div>
@@ -1952,6 +1952,22 @@
</p>
</div>
<!-- Cycle Toggle -->
<div style="display:flex; justify-content:center; margin-bottom:36px;">
<div style="background:var(--bg-secondary); padding:5px; border-radius:12px; display:flex; gap:5px; border:1px solid var(--border);">
<button @click="billingCycle = 'monthly'"
:class="billingCycle === 'monthly' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع شهري
</button>
<button @click="billingCycle = 'annual'"
:class="billingCycle === 'annual' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع سنوي (توفير )
</button>
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(290px, 1fr)); gap:24px;">
<template x-for="p in plans" :key="p.id">
<div class="plan-card" :class="subscription?.plan_id === p.id ? 'active-plan' : ''">
@@ -1970,9 +1986,9 @@
style="text-align:center; padding:18px 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border);">
<span class="num-font"
style="font-size:46px; font-weight:800; color:var(--green-mid);"
x-text="p.price_jod"></span>
x-text="billingCycle === 'monthly' ? (p.price_monthly_jod || p.price_jod) : (p.price_annual_jod || (p.price_jod * 10))"></span>
<span style="font-size:15px; color:var(--text-3); font-weight:500;"> دينار /
شهر</span>
<span x-text="billingCycle === 'monthly' ? 'شهر' : 'سنة'"></span></span>
</div>
<ul
@@ -2712,10 +2728,9 @@
class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">رقم الهاتف <span
class="form-label-sub">(اختياري)</span></label>
<label class="form-label">رقم الهاتف</label>
<input type="text" x-model="newTenant.phone" placeholder="+962 7x xxx xxxx"
class="form-input">
class="form-input" required>
</div>
</div>
</div>
@@ -2731,13 +2746,8 @@
<input type="text" x-model="newTenant.manager_name" placeholder="الاسم الكامل"
class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">بريد المدير الإلكتروني</label>
<input type="email" x-model="newTenant.manager_email" placeholder="manager@office.com"
class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">كلمة مرور الدخول</label>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">كلمة مرور الدخول للمدير</label>
<input type="password" x-model="newTenant.manager_password" placeholder="••••••••"
class="form-input" required>
</div>
@@ -2928,11 +2938,13 @@
isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
acknowledgedWarnings: false, isEditingInvoice: false,
isBusy: false, globalError: '',
isBusy: false,
billingCycle: 'annual', // 'monthly' or 'annual'
globalError: '',
newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' },
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' },
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_password: '' },
connectData: { client_id: '', secret_key: '', income_source_sequence: '1' },
uploadData: { company_id: '' },
currentCompany: null, currentInvoice: null, companyStats: null,
@@ -3103,7 +3115,7 @@
const res = await this.apiRequest('v1/tenants/create', 'POST', this.newTenant);
if (res) {
this.showAddTenantModal = false;
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' };
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_password: '' };
this.loadAll();
alert('تم إضافة المكتب المحاسبي والمدير المسؤول بنجاح');
}
@@ -3400,7 +3412,10 @@
const res = await fetch('/index.php?route=v1/payments/create', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: plan.id })
body: JSON.stringify({
plan_id: plan.id,
billing_cycle: this.billingCycle
})
});
const json = await res.json();
this.isBusy = false;

47
update_annual_plans.sql Normal file
View File

@@ -0,0 +1,47 @@
-- 1. تعطيل الباقات القديمة التي لم نعد نستخدمها
UPDATE subscription_plans SET is_active = 0 WHERE id IN ('office', 'enterprise');
-- 2. تحديث الباقات الحالية إلى باقات سنوية بالأسعار والأرقام الجديدة
UPDATE subscription_plans
SET
name_ar = 'الباقة الأساسية (سنوي)',
name_en = 'Basic Plan (Annual)',
price_jod = 120.00,
max_invoices_month = 12000,
max_companies = 1,
max_users = 1,
is_active = 1
WHERE id = 'basic';
UPDATE subscription_plans
SET
name_ar = 'الباقة الاحترافية (سنوي)',
name_en = 'Pro Plan (Annual)',
price_jod = 250.00,
max_invoices_month = 50000,
max_companies = 9999, -- للشركات غير المحدودة
max_users = 5,
is_active = 1
WHERE id = 'pro';
-- 3. إبقاء الباقة المجانية كما هي
UPDATE subscription_plans
SET
name_ar = 'التجربة المجانية',
name_en = 'Free Trial',
price_jod = 0.00,
max_invoices_month = 15,
max_companies = 1,
max_users = 1,
is_active = 1
WHERE id = 'free';
-- 4. ترحيل وتحديث بيانات العملاء المشتركين حالياً
UPDATE subscriptions s
JOIN subscription_plans sp ON s.plan_id = sp.id
SET
s.max_invoices_per_month = sp.max_invoices_month,
s.max_companies = sp.max_companies,
s.max_users = sp.max_users,
-- تمديد فترة الفوترة للمشتركين المدفوعين لتصبح سنة كاملة من تاريخ بدايتها
s.current_period_end = IF(s.plan_id != 'free', DATE_ADD(s.current_period_start, INTERVAL 1 YEAR), s.current_period_end);

6
update_plans.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/app/bootstrap/init.php';
use App\Core\Database;
$db = Database::getInstance();
$plans = $db->query("SELECT * FROM subscription_plans")->fetchAll();
print_r($plans);

View File

@@ -0,0 +1,46 @@
-- 1. إضافة أعمدة الأسعار (شهري وسنوي) وجدولة الفواتير
ALTER TABLE subscription_plans
ADD COLUMN price_annual_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_jod,
ADD COLUMN price_monthly_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_annual_jod;
-- 2. إضافة دورة الفوترة لجدول الاشتراكات وطلبات الدفع
ALTER TABLE subscriptions
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER status;
ALTER TABLE payment_requests
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER plan_id;
-- 3. تحديث الباقات بالقيم الجديدة المقترحة في التحليل الاستراتيجي
-- الباقة الأساسية
UPDATE subscription_plans
SET
name_ar = 'الباقة الأساسية',
price_annual_jod = 120.00,
price_monthly_jod = 15.00,
max_invoices_month = 500, -- تم تخفيضها من 12000 للتحويل المستقبلي
max_companies = 3, -- تم زيادتها من 1 لجذب المحاسبين المستقلين
max_users = 2,
is_active = 1
WHERE id = 'basic';
-- الباقة الاحترافية
UPDATE subscription_plans
SET
name_ar = 'الباقة الاحترافية',
price_annual_jod = 290.00,
price_monthly_jod = 35.00,
max_invoices_month = 3000,
max_companies = 9999,
max_users = 5,
is_active = 1
WHERE id = 'pro';
-- الباقة المجانية
UPDATE subscription_plans
SET
price_annual_jod = 0.00,
price_monthly_jod = 0.00,
max_invoices_month = 15,
max_companies = 1,
max_users = 1
WHERE id = 'free';