first commit
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Android / Mobile Builds & Configs
|
||||
**/local.properties
|
||||
**/.gradle/
|
||||
**/build/
|
||||
**/.dart_tool/
|
||||
**/.flutter-plugins
|
||||
**/.flutter-plugins-dependencies
|
||||
**/Generated.xcconfig
|
||||
**/flutter_export_environment.sh
|
||||
**/.pub-cache/
|
||||
**/.pub/
|
||||
|
||||
# Logs
|
||||
**/logs/*.log
|
||||
*.log
|
||||
27
backend/.htaccess
Normal file
@@ -0,0 +1,27 @@
|
||||
# Protect includes directory
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Block direct access to includes/
|
||||
RewriteRule ^includes/ - [F,L]
|
||||
|
||||
# Block access to config files
|
||||
RewriteRule ^config\.php$ - [F,L]
|
||||
|
||||
# Block access to hidden files
|
||||
RewriteRule (^|/)\. - [F,L]
|
||||
|
||||
# Block access to SQL files
|
||||
RewriteRule \.sql$ - [F,L]
|
||||
|
||||
# Block access to log files
|
||||
RewriteRule \.log$ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Disable directory listing
|
||||
Options -Indexes
|
||||
|
||||
# Prevent script execution in includes
|
||||
<Directory "includes">
|
||||
php_flag engine off
|
||||
</Directory>
|
||||
121
backend/api/call-done.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/call-done
|
||||
*
|
||||
* Called by Caller Android App after a flash call attempt.
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "task_id": 42,
|
||||
* "device_id": "DEVICE_XXX",
|
||||
* "app_key": "SECRET_DEVICE_KEY",
|
||||
* "result": "success" | "failed" | "busy" | "no_answer"
|
||||
* }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires device key
|
||||
Auth::requireAuth('device');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['task_id']) || !isset($input['device_id']) || !isset($input['result'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
|
||||
RequestLogger::log('call-done', 'POST', $input, 400, 'missing_fields');
|
||||
exit;
|
||||
}
|
||||
|
||||
$taskId = (int) $input['task_id'];
|
||||
$deviceId = trim($input['device_id']);
|
||||
$result = trim($input['result']);
|
||||
|
||||
// Validate result
|
||||
$validResults = ['success', 'failed', 'busy', 'no_answer'];
|
||||
if (!in_array($result, $validResults, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_result_value']);
|
||||
RequestLogger::log('call-done', 'POST', $input, 400, 'invalid_result');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
// Verify this task belongs to this device
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, status FROM otp_requests WHERE id = ? AND device_id = ?"
|
||||
);
|
||||
$stmt->execute([$taskId, $deviceId]);
|
||||
$task = $stmt->fetch();
|
||||
|
||||
if (!$task) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'task_not_found']);
|
||||
RequestLogger::log('call-done', 'POST', $input, 404, 'task_not_found');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($task['status'] !== 'calling') {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'task_not_in_calling_state']);
|
||||
RequestLogger::log('call-done', 'POST', $input, 409, 'wrong_status');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Map result to new status
|
||||
$newStatus = ($result === 'success') ? 'completed' : 'failed';
|
||||
|
||||
$db->beginTransaction();
|
||||
|
||||
// Update OTP request status
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE otp_requests
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE id = ? AND device_id = ?"
|
||||
);
|
||||
$stmt->execute([$newStatus, $taskId, $deviceId]);
|
||||
|
||||
// Increment calls_today for the device
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE caller_devices
|
||||
SET calls_today = calls_today + 1
|
||||
WHERE device_id = ?"
|
||||
);
|
||||
$stmt->execute([$deviceId]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
RequestLogger::log('call-done', 'POST', $input, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$db->rollBack();
|
||||
error_log('call-done error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('call-done', 'POST', $input, 500, $e->getMessage());
|
||||
}
|
||||
124
backend/api/pending-call.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* GET /api/pending-call
|
||||
*
|
||||
* Called by Caller Android App every 3 seconds to fetch the next pending flash call task.
|
||||
*
|
||||
* Query params:
|
||||
* device_id — Registered device identifier
|
||||
* app_key — Device authentication key
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires device key (Caller Android App)
|
||||
Auth::requireAuth('device');
|
||||
|
||||
$deviceId = $_GET['device_id'] ?? null;
|
||||
|
||||
if (!$deviceId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_device_id']);
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 400, 'missing_device_id');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
// Verify device exists and is active
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, device_id, is_active FROM caller_devices WHERE device_id = ?"
|
||||
);
|
||||
$stmt->execute([$deviceId]);
|
||||
$device = $stmt->fetch();
|
||||
|
||||
if (!$device || !$device['is_active']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'device_not_registered_or_inactive']);
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 403, 'invalid_device');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Update last_seen
|
||||
$stmt = $db->prepare("UPDATE caller_devices SET last_seen = NOW() WHERE device_id = ?");
|
||||
$stmt->execute([$deviceId]);
|
||||
|
||||
// Find oldest pending flash call task
|
||||
// Priority: tasks assigned to this device first, then unassigned tasks
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, phone, caller_id, expires_at
|
||||
FROM otp_requests
|
||||
WHERE method = 'flash_call'
|
||||
AND status = 'pending'
|
||||
AND expires_at > NOW()
|
||||
AND (device_id IS NULL OR device_id = ?)
|
||||
ORDER BY
|
||||
CASE WHEN device_id = ? THEN 0 ELSE 1 END,
|
||||
created_at ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$deviceId, $deviceId]);
|
||||
$task = $stmt->fetch();
|
||||
|
||||
if (!$task) {
|
||||
echo json_encode(['success' => true, 'task_id' => null]);
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Claim this task — update status and assign device
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE otp_requests
|
||||
SET status = 'calling', device_id = ?, updated_at = NOW()
|
||||
WHERE id = ? AND status = 'pending'"
|
||||
);
|
||||
$stmt->execute([$deviceId, $task['id']]);
|
||||
|
||||
// Check if update affected any row (race condition handling)
|
||||
if ($stmt->rowCount() === 0) {
|
||||
// Another device claimed it first
|
||||
echo json_encode(['success' => true, 'task_id' => null]);
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Calculate remaining timeout
|
||||
$expiresAt = new \DateTime($task['expires_at']);
|
||||
$now = new \DateTime();
|
||||
$timeoutSeconds = max(10, $expiresAt->getTimestamp() - $now->getTimestamp());
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'task_id' => (int) $task['id'],
|
||||
'phone' => $task['phone'],
|
||||
'caller_id' => $task['caller_id'],
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
]);
|
||||
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log('pending-call error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('pending-call', 'GET', $_GET, 500, $e->getMessage());
|
||||
}
|
||||
121
backend/api/pending-sms.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* GET /api/pending-sms
|
||||
*
|
||||
* Called by Caller Android App every 3 seconds to fetch the next pending SMS task.
|
||||
*
|
||||
* Query params:
|
||||
* device_id — Registered device identifier
|
||||
* app_key — Device authentication key
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires device key
|
||||
Auth::requireAuth('device');
|
||||
|
||||
$deviceId = $_GET['device_id'] ?? null;
|
||||
|
||||
if (!$deviceId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_device_id']);
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 400, 'missing_device_id');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
// Verify device exists and is active
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, device_id, is_active FROM caller_devices WHERE device_id = ?"
|
||||
);
|
||||
$stmt->execute([$deviceId]);
|
||||
$device = $stmt->fetch();
|
||||
|
||||
if (!$device || !$device['is_active']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'device_not_registered_or_inactive']);
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 403, 'invalid_device');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Update last_seen
|
||||
$stmt = $db->prepare("UPDATE caller_devices SET last_seen = NOW() WHERE device_id = ?");
|
||||
$stmt->execute([$deviceId]);
|
||||
|
||||
// Find oldest pending SMS task
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, phone, otp_code, expires_at
|
||||
FROM otp_requests
|
||||
WHERE method = 'sms'
|
||||
AND status = 'pending_sms'
|
||||
AND expires_at > NOW()
|
||||
AND (device_id IS NULL OR device_id = ?)
|
||||
ORDER BY
|
||||
CASE WHEN device_id = ? THEN 0 ELSE 1 END,
|
||||
created_at ASC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$deviceId, $deviceId]);
|
||||
$task = $stmt->fetch();
|
||||
|
||||
if (!$task) {
|
||||
echo json_encode(['success' => true, 'task_id' => null]);
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Claim this task
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE otp_requests
|
||||
SET status = 'calling', device_id = ?, updated_at = NOW()
|
||||
WHERE id = ? AND status = 'pending_sms'"
|
||||
);
|
||||
$stmt->execute([$deviceId, $task['id']]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
echo json_encode(['success' => true, 'task_id' => null]);
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Calculate remaining timeout
|
||||
$expiresAt = new \DateTime($task['expires_at']);
|
||||
$now = new \DateTime();
|
||||
$timeoutSeconds = max(10, $expiresAt->getTimestamp() - $now->getTimestamp());
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'task_id' => (int) $task['id'],
|
||||
'phone' => $task['phone'],
|
||||
'otp' => $task['otp_code'],
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
]);
|
||||
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log('pending-sms error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('pending-sms', 'GET', $_GET, 500, $e->getMessage());
|
||||
}
|
||||
120
backend/api/register-device.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/register-device
|
||||
*
|
||||
* Register a Caller Android device.
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "device_id": "DEVICE_XXX",
|
||||
* "phone_number": "+9627XXXXXXXX",
|
||||
* "sim_slot": 0,
|
||||
* "app_key": "SECRET_DEVICE_KEY"
|
||||
* }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires device key
|
||||
Auth::requireAuth('device');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['device_id']) || !isset($input['phone_number'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
|
||||
RequestLogger::log('register-device', 'POST', $input, 400, 'missing_fields');
|
||||
exit;
|
||||
}
|
||||
|
||||
$deviceId = trim($input['device_id']);
|
||||
$phoneNumber = trim($input['phone_number']);
|
||||
$simSlot = isset($input['sim_slot']) ? (int) $input['sim_slot'] : 0;
|
||||
|
||||
// Validate device_id
|
||||
if (strlen($deviceId) < 5 || strlen($deviceId) > 50) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_device_id_length']);
|
||||
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_device_id');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate phone format
|
||||
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phoneNumber)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_phone_format']);
|
||||
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_phone');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate sim_slot
|
||||
if ($simSlot < 0 || $simSlot > 3) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_sim_slot']);
|
||||
RequestLogger::log('register-device', 'POST', $input, 400, 'invalid_sim_slot');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
// Check if device already registered
|
||||
$stmt = $db->prepare("SELECT id, is_active FROM caller_devices WHERE device_id = ?");
|
||||
$stmt->execute([$deviceId]);
|
||||
$existing = $stmt->fetch();
|
||||
|
||||
if ($existing) {
|
||||
// Update existing device (re-registration)
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE caller_devices
|
||||
SET phone_number = ?, sim_slot = ?, is_active = 1, last_seen = NOW()
|
||||
WHERE device_id = ?"
|
||||
);
|
||||
$stmt->execute([$phoneNumber, $simSlot, $deviceId]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'device_updated',
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
} else {
|
||||
// Insert new device
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO caller_devices (device_id, phone_number, sim_slot, is_active, last_seen, calls_today, created_at)
|
||||
VALUES (?, ?, ?, 1, NOW(), 0, NOW())"
|
||||
);
|
||||
$stmt->execute([$deviceId, $phoneNumber, $simSlot]);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'device_registered',
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
RequestLogger::log('register-device', 'POST', $input, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log('register-device error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('register-device', 'POST', $input, 500, $e->getMessage());
|
||||
}
|
||||
190
backend/api/request-otp.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/request-otp
|
||||
*
|
||||
* Request an OTP to be delivered via flash call (Android) or SMS (iOS).
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "phone": "+9627XXXXXXXX",
|
||||
* "app_key": "SECRET_APP_KEY",
|
||||
* "device_type": "android" | "ios" (optional, defaults to "android")
|
||||
* }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
// Handle CORS preflight
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Redis.php';
|
||||
require_once __DIR__ . '/../includes/RateLimit.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires app key (Flutter app)
|
||||
Auth::requireAuth('app');
|
||||
|
||||
// Parse request body
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['phone'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_phone']);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 400, 'missing_phone');
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = trim($input['phone']);
|
||||
$deviceType = isset($input['device_type']) ? strtolower(trim($input['device_type'])) : 'android';
|
||||
|
||||
// Validate device_type
|
||||
if (!in_array($deviceType, ['android', 'ios'], true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_device_type']);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_device_type');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate phone format (E.164)
|
||||
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'invalid_phone_format',
|
||||
'hint' => 'Phone must be in E.164 format, e.g. +9627XXXXXXXX',
|
||||
]);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_phone_format');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate limit check: max 3 requests per phone per 10 minutes
|
||||
$rateLimit = new RateLimit();
|
||||
if (!$rateLimit->check("otp:{$phone}")) {
|
||||
$remaining = $rateLimit->remaining("otp:{$phone}");
|
||||
$ttl = $rateLimit->ttl("otp:{$phone}");
|
||||
http_response_code(429);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'rate_limit_exceeded',
|
||||
'retry_after' => $ttl,
|
||||
'remaining' => $remaining,
|
||||
]);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 429, 'rate_limit_exceeded');
|
||||
exit;
|
||||
}
|
||||
|
||||
// IP-based rate limiting
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['success' => false, 'message' => 'ip_rate_limit_exceeded']);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 429, 'ip_rate_limit');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate 4-digit OTP (cryptographically secure)
|
||||
$otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
|
||||
// Determine delivery method
|
||||
$method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
|
||||
|
||||
$db = Database::getInstance();
|
||||
$redis = RedisClient::getInstance();
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
if ($method === 'flash_call') {
|
||||
// Find available caller device (round-robin)
|
||||
$stmt = $db->prepare(
|
||||
"SELECT device_id, phone_number, sim_slot
|
||||
FROM caller_devices
|
||||
WHERE is_active = 1
|
||||
ORDER BY calls_today ASC, last_seen DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute();
|
||||
$device = $stmt->fetch();
|
||||
|
||||
if (!$device) {
|
||||
$db->rollBack();
|
||||
http_response_code(503);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'no_caller_devices_available',
|
||||
]);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 503, 'no_caller_devices');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build caller_id: +96279XX{OTP}
|
||||
$randomDigits = str_pad((string) random_int(0, 99), 2, '0', STR_PAD_LEFT);
|
||||
$callerId = CALLER_ID_PREFIX . $randomDigits . $otpCode;
|
||||
|
||||
// Insert OTP request
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, device_id, method, expires_at)
|
||||
VALUES (?, ?, ?, 'pending', ?, 'flash_call', ?)"
|
||||
);
|
||||
$stmt->execute([$phone, $otpCode, $callerId, $device['device_id'], $expiresAt]);
|
||||
} else {
|
||||
// SMS delivery — no specific caller_id needed for the OTP request
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at)
|
||||
VALUES (?, ?, '', 'pending_sms', 'sms', ?)"
|
||||
);
|
||||
$stmt->execute([$phone, $otpCode, $expiresAt]);
|
||||
}
|
||||
|
||||
$otpId = $db->lastInsertId();
|
||||
$db->commit();
|
||||
|
||||
// Store OTP in Redis with TTL
|
||||
$redisKey = "otp:{$phone}";
|
||||
$redis->setex($redisKey, OTP_EXPIRE_SECONDS, json_encode([
|
||||
'otp' => $otpCode,
|
||||
'method' => $method,
|
||||
'attempts' => 0,
|
||||
'created' => time(),
|
||||
]));
|
||||
|
||||
// Response
|
||||
$response = [
|
||||
'success' => true,
|
||||
'otp_id' => $otpId,
|
||||
'otp' => $otpCode,
|
||||
'expires_in' => OTP_EXPIRE_SECONDS,
|
||||
'method' => $method,
|
||||
];
|
||||
|
||||
if ($method === 'flash_call' && isset($device)) {
|
||||
$response['caller_device_id'] = $device['device_id'];
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
RequestLogger::log('request-otp', 'POST', $input, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$db->rollBack();
|
||||
error_log('request-otp error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('request-otp', 'POST', $input, 500, $e->getMessage());
|
||||
}
|
||||
120
backend/api/sms-done.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/sms-done
|
||||
*
|
||||
* Called by Caller Android App after sending an SMS OTP.
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "task_id": 43,
|
||||
* "device_id": "DEVICE_XXX",
|
||||
* "app_key": "SECRET_DEVICE_KEY",
|
||||
* "result": "success" | "failed"
|
||||
* }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires device key
|
||||
Auth::requireAuth('device');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['task_id']) || !isset($input['device_id']) || !isset($input['result'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_required_fields']);
|
||||
RequestLogger::log('sms-done', 'POST', $input, 400, 'missing_fields');
|
||||
exit;
|
||||
}
|
||||
|
||||
$taskId = (int) $input['task_id'];
|
||||
$deviceId = trim($input['device_id']);
|
||||
$result = trim($input['result']);
|
||||
|
||||
// Validate result
|
||||
$validResults = ['success', 'failed'];
|
||||
if (!in_array($result, $validResults, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_result_value']);
|
||||
RequestLogger::log('sms-done', 'POST', $input, 400, 'invalid_result');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
// Verify this task belongs to this device
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, status, method FROM otp_requests WHERE id = ? AND device_id = ?"
|
||||
);
|
||||
$stmt->execute([$taskId, $deviceId]);
|
||||
$task = $stmt->fetch();
|
||||
|
||||
if (!$task) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'task_not_found']);
|
||||
RequestLogger::log('sms-done', 'POST', $input, 404, 'task_not_found');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($task['status'] !== 'calling') {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'message' => 'task_not_in_calling_state']);
|
||||
RequestLogger::log('sms-done', 'POST', $input, 409, 'wrong_status');
|
||||
exit;
|
||||
}
|
||||
|
||||
$newStatus = ($result === 'success') ? 'completed' : 'failed';
|
||||
|
||||
$db->beginTransaction();
|
||||
|
||||
// Update OTP request status
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE otp_requests
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE id = ? AND device_id = ?"
|
||||
);
|
||||
$stmt->execute([$newStatus, $taskId, $deviceId]);
|
||||
|
||||
// Increment calls_today (counts both calls and SMS)
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE caller_devices
|
||||
SET calls_today = calls_today + 1
|
||||
WHERE device_id = ?"
|
||||
);
|
||||
$stmt->execute([$deviceId]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
RequestLogger::log('sms-done', 'POST', $input, 200);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$db->rollBack();
|
||||
error_log('sms-done error: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
||||
RequestLogger::log('sms-done', 'POST', $input, 500, $e->getMessage());
|
||||
}
|
||||
154
backend/api/verify-otp.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /api/verify-otp
|
||||
*
|
||||
* Verify an OTP code submitted by the user (Flutter app).
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "phone": "+9627XXXXXXXX",
|
||||
* "otp": "4829",
|
||||
* "app_key": "SECRET_APP_KEY"
|
||||
* }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Redis.php';
|
||||
require_once __DIR__ . '/../includes/RateLimit.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
|
||||
// Authenticate — requires app key
|
||||
Auth::requireAuth('app');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['phone']) || !isset($input['otp'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'missing_phone_or_otp']);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 400, 'missing_fields');
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = trim($input['phone']);
|
||||
$otp = trim($input['otp']);
|
||||
|
||||
// Validate phone format
|
||||
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_phone_format']);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_phone');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate OTP format (4 digits)
|
||||
if (!preg_match('/^\d{4}$/', $otp)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'invalid_otp_format']);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_otp_format');
|
||||
exit;
|
||||
}
|
||||
|
||||
$redis = RedisClient::getInstance();
|
||||
$db = Database::getInstance();
|
||||
$redisKey = "otp:{$phone}";
|
||||
|
||||
// Check if OTP exists in Redis
|
||||
$stored = $redis->get($redisKey);
|
||||
|
||||
if ($stored === false || $stored === null) {
|
||||
// OTP expired or never existed
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'expired',
|
||||
]);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 200, 'expired');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Decode stored data
|
||||
if (is_string($stored)) {
|
||||
$otpData = json_decode($stored, true);
|
||||
} else {
|
||||
$otpData = $stored;
|
||||
}
|
||||
|
||||
if (!is_array($otpData) || !isset($otpData['otp'])) {
|
||||
// Corrupted data — clean up
|
||||
$redis->del($redisKey);
|
||||
echo json_encode(['success' => false, 'message' => 'expired']);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 200, 'corrupted_data');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check max attempts
|
||||
if (isset($otpData['attempts']) && (int) $otpData['attempts'] >= MAX_OTP_ATTEMPTS) {
|
||||
$redis->del($redisKey);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'max_attempts',
|
||||
]);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 200, 'max_attempts');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Increment attempt counter
|
||||
$otpData['attempts'] = (int) ($otpData['attempts'] ?? 0) + 1;
|
||||
$ttl = $redis->ttl($redisKey);
|
||||
if ($ttl > 0) {
|
||||
$redis->setex($redisKey, $ttl, json_encode($otpData));
|
||||
}
|
||||
|
||||
// Timing-safe comparison
|
||||
if (hash_equals($otpData['otp'], $otp)) {
|
||||
// Success — clean up Redis
|
||||
$redis->del($redisKey);
|
||||
|
||||
// Update database
|
||||
try {
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE otp_requests
|
||||
SET verified_at = NOW(), status = 'verified'
|
||||
WHERE phone = ? AND otp_code = ? AND verified_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$phone, $otp]);
|
||||
} catch (\Throwable $e) {
|
||||
// Non-critical — OTP is already verified via Redis
|
||||
error_log('verify-otp DB update error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'verified',
|
||||
]);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 200);
|
||||
|
||||
} else {
|
||||
// Wrong OTP
|
||||
$remainingAttempts = MAX_OTP_ATTEMPTS - (int) $otpData['attempts'];
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'invalid_otp',
|
||||
'remaining_attempts' => max(0, $remainingAttempts),
|
||||
]);
|
||||
RequestLogger::log('verify-otp', 'POST', $input, 200, 'invalid_otp');
|
||||
}
|
||||
43
backend/config.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Flash Call OTP System — Configuration
|
||||
* Domain: otp.intaleqapp.com
|
||||
*/
|
||||
|
||||
// Database
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'otp_db');
|
||||
define('DB_USER', 'otp_user');
|
||||
define('DB_PASS', 'STRONG_PASSWORD');
|
||||
|
||||
// Redis
|
||||
define('REDIS_HOST', '127.0.0.1');
|
||||
define('REDIS_PORT', 6379);
|
||||
define('REDIS_PASSWORD', null); // Set if Redis requires auth
|
||||
define('REDIS_DB', 1); // Isolated DB for OTP system
|
||||
|
||||
// Application Keys
|
||||
define('APP_KEY', 'f3a9e7c1b8d5f2a4c6e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1');
|
||||
define('DEVICE_KEY', 'er4er4');
|
||||
|
||||
// SMS Sender ID (for iOS SMS delivery)
|
||||
define('SMS_SENDER_ID', 'انطلق');
|
||||
|
||||
// OTP Settings
|
||||
define('OTP_EXPIRE_SECONDS', 120);
|
||||
define('MAX_OTP_ATTEMPTS', 5);
|
||||
|
||||
// Rate Limiting
|
||||
define('RATE_LIMIT_WINDOW', 600); // 10 minutes in seconds
|
||||
define('RATE_LIMIT_MAX', 3); // Max OTP requests per phone per window
|
||||
|
||||
// Caller ID Configuration
|
||||
// Format: +96279XX{OTP} — XX = random 2 digits
|
||||
define('CALLER_ID_PREFIX', '+96279');
|
||||
|
||||
// Logging
|
||||
define('LOG_REQUESTS', true);
|
||||
define('LOG_FILE', __DIR__ . '/logs/api.log');
|
||||
|
||||
// Timezone
|
||||
date_default_timezone_set('Asia/Amman');
|
||||
110
backend/database.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- ============================================
|
||||
-- Flash Call OTP System — Database Setup
|
||||
-- Domain: otp.intaleqapp.com
|
||||
-- ============================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS otpDb
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE otpDb;
|
||||
|
||||
-- OTP Requests Table
|
||||
CREATE TABLE IF NOT EXISTS otp_requests (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
otp_code VARCHAR(6) NOT NULL,
|
||||
caller_id VARCHAR(20) NOT NULL DEFAULT '',
|
||||
status ENUM('pending','pending_sms','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending',
|
||||
method ENUM('flash_call','sms') NOT NULL DEFAULT 'flash_call',
|
||||
device_id VARCHAR(50) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
verified_at TIMESTAMP NULL,
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_method_status (method, status),
|
||||
INDEX idx_device_id (device_id),
|
||||
INDEX idx_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Caller Devices Table
|
||||
CREATE TABLE IF NOT EXISTS caller_devices (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
phone_number VARCHAR(20) NOT NULL,
|
||||
sim_slot TINYINT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_seen TIMESTAMP NULL,
|
||||
calls_today INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_active (is_active),
|
||||
INDEX idx_calls_today (calls_today)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- API Request Logs Table
|
||||
CREATE TABLE IF NOT EXISTS api_logs (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
endpoint VARCHAR(50) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
user_agent VARCHAR(500) NULL,
|
||||
request_body TEXT NULL,
|
||||
response_code SMALLINT NOT NULL DEFAULT 200,
|
||||
error VARCHAR(500) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_endpoint (endpoint),
|
||||
INDEX idx_created (created_at),
|
||||
INDEX idx_ip (ip_address)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================
|
||||
-- Scheduled Cleanup (add to MySQL Event Scheduler)
|
||||
-- ============================================
|
||||
|
||||
-- Enable event scheduler
|
||||
SET GLOBAL event_scheduler = ON;
|
||||
|
||||
-- Expire old pending requests every 30 seconds
|
||||
DELIMITER //
|
||||
CREATE EVENT IF NOT EXISTS expire_otp_requests
|
||||
ON SCHEDULE EVERY 30 SECOND
|
||||
DO
|
||||
BEGIN
|
||||
UPDATE otp_requests
|
||||
SET status = 'expired', updated_at = NOW()
|
||||
WHERE status IN ('pending', 'pending_sms', 'calling')
|
||||
AND expires_at < NOW();
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
-- Reset calls_today counter daily at midnight (Asia/Amman = UTC+3)
|
||||
DELIMITER //
|
||||
CREATE EVENT IF NOT EXISTS reset_daily_calls
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
STARTS CONCAT(CURDATE() + INTERVAL 1 DAY, ' 00:00:00')
|
||||
DO
|
||||
BEGIN
|
||||
UPDATE caller_devices SET calls_today = 0;
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
-- Clean up old API logs (older than 30 days)
|
||||
DELIMITER //
|
||||
CREATE EVENT IF NOT EXISTS cleanup_api_logs
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
STARTS CONCAT(CURDATE() + INTERVAL 1 DAY, ' 03:00:00')
|
||||
DO
|
||||
BEGIN
|
||||
DELETE FROM api_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
-- ============================================
|
||||
-- Grant permissions
|
||||
-- ============================================
|
||||
-- Run this separately with root access:
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON otpDb.* TO 'otpUser'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD';
|
||||
-- FLUSH PRIVILEGES;
|
||||
sfvo055OJV7VqPW25VEQ
|
||||
96
backend/includes/Auth.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication — App Key Validation
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* Validate the app_key from request.
|
||||
* Supports both Flutter app key and Caller device key.
|
||||
*
|
||||
* @param string|null $key The key provided in request
|
||||
* @param string $required Which key type is required: 'app' | 'device' | 'any'
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate(?string $key, string $required = 'any'): bool
|
||||
{
|
||||
if ($key === null || $key === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($required) {
|
||||
case 'app':
|
||||
return hash_equals(APP_KEY, $key);
|
||||
case 'device':
|
||||
return hash_equals(DEVICE_KEY, $key);
|
||||
case 'any':
|
||||
return hash_equals(APP_KEY, $key) || hash_equals(DEVICE_KEY, $key);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract app_key from request (header or body).
|
||||
*/
|
||||
public static function getKeyFromRequest(): ?string
|
||||
{
|
||||
// Check header first
|
||||
$headerKey = $_SERVER['HTTP_X_APP_KEY']
|
||||
?? $_SERVER['HTTP_APP_KEY']
|
||||
?? null;
|
||||
|
||||
if ($headerKey !== null) {
|
||||
return $headerKey;
|
||||
}
|
||||
|
||||
// Check JSON body
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (is_array($body) && isset($body['app_key'])) {
|
||||
return $body['app_key'];
|
||||
}
|
||||
|
||||
// Check POST data
|
||||
if (isset($_POST['app_key'])) {
|
||||
return $_POST['app_key'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication — sends 401 and exits on failure.
|
||||
*/
|
||||
public static function requireAuth(string $required = 'any'): void
|
||||
{
|
||||
$key = self::getKeyFromRequest();
|
||||
if (!self::validate($key, $required)) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'invalid_app_key',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the provided key is the device key.
|
||||
*/
|
||||
public static function isDeviceKey(?string $key): bool
|
||||
{
|
||||
return $key !== null && hash_equals(DEVICE_KEY, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the provided key is the app key.
|
||||
*/
|
||||
public static function isAppKey(?string $key): bool
|
||||
{
|
||||
return $key !== null && hash_equals(APP_KEY, $key);
|
||||
}
|
||||
}
|
||||
49
backend/includes/Database.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Singleton — PDO Connection
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
try {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=utf8mb4',
|
||||
DB_HOST,
|
||||
DB_NAME
|
||||
);
|
||||
|
||||
self::$instance = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+03:00'",
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log('Database connection failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'database_error']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Prevent cloning */
|
||||
private function __clone() {}
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \Exception("Cannot unserialize singleton");
|
||||
}
|
||||
}
|
||||
61
backend/includes/Logger.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Request Logger — Logs all API requests to MySQL
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/Database.php';
|
||||
|
||||
class RequestLogger
|
||||
{
|
||||
/**
|
||||
* Log an API request.
|
||||
*/
|
||||
public static function log(
|
||||
string $endpoint,
|
||||
string $method,
|
||||
?array $requestBody = null,
|
||||
int $responseCode = 200,
|
||||
?string $error = null
|
||||
): void {
|
||||
if (!LOG_REQUESTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||
$body = $requestBody ? json_encode($requestBody) : null;
|
||||
|
||||
// Mask sensitive fields
|
||||
if ($body) {
|
||||
$body = self::maskSensitive($body);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO api_logs (endpoint, method, ip_address, user_agent, request_body, response_code, error, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"
|
||||
);
|
||||
$stmt->execute([$endpoint, $method, $ip, $userAgent, $body, $responseCode, $error]);
|
||||
} catch (\Throwable $e) {
|
||||
// Logging should never break the app
|
||||
error_log("RequestLogger error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask sensitive fields in request body.
|
||||
*/
|
||||
private static function maskSensitive(string $body): string
|
||||
{
|
||||
$sensitive = ['app_key', 'password', 'otp', 'otp_code'];
|
||||
foreach ($sensitive as $field) {
|
||||
$body = preg_replace(
|
||||
'/"' . $field . '"\s*:\s*"[^"]*"/',
|
||||
'"' . $field . '":"***"',
|
||||
$body
|
||||
);
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
62
backend/includes/RateLimit.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Rate Limiting via Redis
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/Redis.php';
|
||||
|
||||
class RateLimit
|
||||
{
|
||||
private \Redis $redis;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->redis = RedisClient::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and increment rate limit counter.
|
||||
*
|
||||
* @param string $key Identifier (e.g. "otp:+9627XXXXXXXX")
|
||||
* @param int $limit Max requests allowed
|
||||
* @param int $window Time window in seconds
|
||||
* @return bool true = allowed, false = rate limited
|
||||
*/
|
||||
public function check(string $key, int $limit = RATE_LIMIT_MAX, int $window = RATE_LIMIT_WINDOW): bool
|
||||
{
|
||||
return true; // Disabled for stress testing
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining requests for a key.
|
||||
*/
|
||||
public function remaining(string $key, int $limit = RATE_LIMIT_MAX): int
|
||||
{
|
||||
$redisKey = "rate_limit:{$key}";
|
||||
$current = (int) $this->redis->get($redisKey);
|
||||
return max(0, $limit - $current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TTL of rate limit key.
|
||||
*/
|
||||
public function ttl(string $key): int
|
||||
{
|
||||
$redisKey = "rate_limit:{$key}";
|
||||
return max(0, (int) $this->redis->ttl($redisKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* General IP-based rate limiting for API endpoints.
|
||||
*
|
||||
* @param string $ip Client IP
|
||||
* @param string $endpoint Endpoint name
|
||||
* @param int $limit Max requests
|
||||
* @param int $window Time window in seconds
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIp(string $ip, string $endpoint, int $limit = 60, int $window = 60): bool
|
||||
{
|
||||
return $this->check("ip:{$endpoint}:{$ip}", $limit, $window);
|
||||
}
|
||||
}
|
||||
55
backend/includes/Redis.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Redis Singleton — phpredis Extension
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
class RedisClient
|
||||
{
|
||||
private static ?\Redis $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
try {
|
||||
self::$instance = new \Redis();
|
||||
self::$instance->connect(REDIS_HOST, REDIS_PORT, 2.0);
|
||||
|
||||
if (REDIS_PASSWORD !== null) {
|
||||
self::$instance->auth(REDIS_PASSWORD);
|
||||
}
|
||||
|
||||
if (REDIS_DB > 0) {
|
||||
self::$instance->select(REDIS_DB);
|
||||
}
|
||||
|
||||
self::$instance->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON);
|
||||
} catch (\RedisException $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
error_log('Redis connection failed: ' . $errorMsg);
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
error_log('Unexpected error in Redis: ' . $errorMsg);
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): \Redis
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __clone() {}
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \Exception("Cannot unserialize singleton");
|
||||
}
|
||||
}
|
||||
0
backend/logs/.gitkeep
Normal file
95
backend/nginx.conf
Normal file
@@ -0,0 +1,95 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name otp.intaleqapp.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name otp.intaleqapp.com;
|
||||
|
||||
# SSL — Let's Encrypt
|
||||
ssl_certificate /etc/letsencrypt/live/otp.intaleqapp.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/otp.intaleqapp.com/privkey.pem;
|
||||
|
||||
# SSL Hardening
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
|
||||
# Document root
|
||||
root /var/www/otp.intaleqapp.com/public;
|
||||
index index.php;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/otp.intaleqapp.com.access.log;
|
||||
error_log /var/log/nginx/otp.intaleqapp.com.error.log;
|
||||
|
||||
# API endpoints — route to PHP files
|
||||
location /api/ {
|
||||
try_files $uri $uri/ /api$uri.php?$query_string;
|
||||
|
||||
# PHP handling
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_read_timeout 30;
|
||||
}
|
||||
}
|
||||
|
||||
# Block direct access to includes/
|
||||
location /includes/ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block access to config
|
||||
location /config.php {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block access to hidden files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block access to SQL files
|
||||
location ~ \.sql$ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block access to log files
|
||||
location ~ \.log$ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 '{"status":"ok"}';
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
# Default PHP handling
|
||||
location ~ \.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
# Deny everything else
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
48
caller-app/app/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.intaleq.flashcall'
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.intaleq.flashcall"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.10.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
|
||||
// The Kotlin plugin automatically adds the correct stdlib version.
|
||||
// Invalid version 2.3.10 was removed.
|
||||
}
|
||||
1
caller-app/app/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
android.useAndroidX=true
|
||||
34
caller-app/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# Retrofit
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepclassmembers,allowshrinking,allowobfuscation interface * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
# Gson
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Keep data classes for Gson serialization
|
||||
-keep class com.intaleq.flashcall.PendingTask { *; }
|
||||
-keep class com.intaleq.flashcall.CallDoneRequest { *; }
|
||||
-keep class com.intaleq.flashcall.RegisterRequest { *; }
|
||||
-keep class com.intaleq.flashcall.ApiResponse { *; }
|
||||
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
|
||||
# Kotlin Coroutines
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
|
||||
-keepclassmembers class kotlinx.coroutines.** {
|
||||
volatile <fields>;
|
||||
}
|
||||
55
caller-app/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Flash OTP Caller"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
|
||||
android:usesCleartextTraffic="false">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".CallerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Automation of flash call verification for OTP purposes" />
|
||||
</service>
|
||||
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
caller-app/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,75 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import retrofit2.http.*
|
||||
|
||||
// --- Data Models ---
|
||||
|
||||
data class PendingTask(
|
||||
@SerializedName("task_id") val taskId: Int?,
|
||||
@SerializedName("phone") val phone: String?,
|
||||
@SerializedName("caller_id") val callerId: String?,
|
||||
@SerializedName("otp") val otp: String?,
|
||||
@SerializedName("timeout_seconds") val timeoutSeconds: Int?
|
||||
)
|
||||
|
||||
data class CallDoneRequest(
|
||||
@SerializedName("task_id") val taskId: Int,
|
||||
@SerializedName("device_id") val deviceId: String,
|
||||
@SerializedName("app_key") val appKey: String,
|
||||
@SerializedName("result") val result: String
|
||||
)
|
||||
|
||||
data class RegisterRequest(
|
||||
@SerializedName("device_id") val deviceId: String,
|
||||
@SerializedName("phone_number") val phoneNumber: String,
|
||||
@SerializedName("sim_slot") val simSlot: Int,
|
||||
@SerializedName("app_key") val appKey: String
|
||||
)
|
||||
|
||||
data class ApiResponse(
|
||||
@SerializedName("success") val success: Boolean,
|
||||
@SerializedName("message") val message: String?,
|
||||
@SerializedName("device_id") val deviceId: String?
|
||||
)
|
||||
|
||||
// --- API Interface ---
|
||||
|
||||
interface ApiService {
|
||||
|
||||
/**
|
||||
* Polling for a new flash call task
|
||||
*/
|
||||
@GET("pending-call.php")
|
||||
suspend fun pendingCall(
|
||||
@Query("device_id") deviceId: String,
|
||||
@Query("app_key") appKey: String
|
||||
): PendingTask
|
||||
|
||||
/**
|
||||
* Polling for a new SMS task
|
||||
*/
|
||||
@GET("pending-sms.php")
|
||||
suspend fun pendingSms(
|
||||
@Query("device_id") deviceId: String,
|
||||
@Query("app_key") appKey: String
|
||||
): PendingTask
|
||||
|
||||
/**
|
||||
* Reporting the result of a flash call
|
||||
*/
|
||||
@POST("call-done.php")
|
||||
suspend fun callDone(@Body request: CallDoneRequest): ApiResponse
|
||||
|
||||
/**
|
||||
* Reporting the result of an SMS
|
||||
*/
|
||||
@POST("sms-done.php")
|
||||
suspend fun smsDone(@Body request: CallDoneRequest): ApiResponse
|
||||
|
||||
/**
|
||||
* Initial registration of the device
|
||||
*/
|
||||
@POST("register-device.php")
|
||||
suspend fun registerDevice(@Body request: RegisterRequest): ApiResponse
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
|
||||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
|
||||
|
||||
val prefs = context.getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
|
||||
val isRegistered = prefs.getBoolean("is_registered", false)
|
||||
|
||||
if (isRegistered) {
|
||||
val serviceIntent = Intent(context, CallerService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent)
|
||||
} else {
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.telephony.SmsManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class CallerService : Service() {
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private lateinit var flashCallManager: FlashCallManager
|
||||
private var callPollerJob: Job? = null
|
||||
private var smsPollerJob: Job? = null
|
||||
|
||||
companion object {
|
||||
var isRunning = false
|
||||
private set
|
||||
|
||||
// Callback for MainActivity to receive log messages
|
||||
var logListener: ((String) -> Unit)? = null
|
||||
|
||||
private const val CHANNEL_ID = "flash_call_service_channel"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
}
|
||||
|
||||
private val notificationManager by lazy {
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isRunning = true
|
||||
flashCallManager = FlashCallManager(this)
|
||||
createNotificationChannel()
|
||||
// startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
|
||||
// ... inside onCreate() ...
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
buildNotification("Flash OTP Caller — Active"),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
|
||||
}
|
||||
|
||||
// Register SMS hardware status receiver
|
||||
val filter = android.content.IntentFilter("SMS_SENT")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(smsReceiver, filter)
|
||||
}
|
||||
|
||||
startPolling()
|
||||
addLog("Service started")
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
callPollerJob?.cancel()
|
||||
smsPollerJob?.cancel()
|
||||
serviceScope.cancel()
|
||||
try {
|
||||
unregisterReceiver(smsReceiver)
|
||||
} catch (e: Exception) {
|
||||
// Ignore if not registered
|
||||
}
|
||||
isRunning = false
|
||||
addLog("Service stopped")
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
callPollerJob = serviceScope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
pollCallTask()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
addLog("Call poll error: ${e.message}")
|
||||
}
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
|
||||
smsPollerJob = serviceScope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
pollSmsTask()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
addLog("SMS poll error: ${e.message}")
|
||||
}
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pollCallTask() {
|
||||
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
|
||||
val deviceId = prefs.getString("device_id", null) ?: return
|
||||
val appKey = prefs.getString("app_key", null) ?: return
|
||||
|
||||
RetrofitClient.setAppKey(appKey)
|
||||
val api = RetrofitClient.apiService
|
||||
|
||||
val task = try {
|
||||
val result = api.pendingCall(deviceId, appKey)
|
||||
addLog("Call poll OK: taskId=${result.taskId}")
|
||||
result
|
||||
} catch (e: retrofit2.HttpException) {
|
||||
addLog("Call poll HTTP ${e.code()}: ${e.message()}")
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
addLog("Call poll net error: ${e.javaClass.simpleName}: ${e.message}")
|
||||
return
|
||||
}
|
||||
|
||||
val taskId = task.taskId ?: return
|
||||
val phone = task.phone ?: return
|
||||
|
||||
addLog("Call task #$taskId → $phone")
|
||||
|
||||
val result = flashCallManager.makeFlashCall(phone)
|
||||
addLog("Call #$taskId result: $result")
|
||||
|
||||
// Report result back to server
|
||||
try {
|
||||
api.callDone(
|
||||
CallDoneRequest(
|
||||
taskId = taskId,
|
||||
deviceId = deviceId,
|
||||
appKey = appKey,
|
||||
result = result
|
||||
)
|
||||
)
|
||||
addLog("Call #$taskId reported: $result")
|
||||
} catch (e: Exception) {
|
||||
addLog("Call #$taskId report failed: ${e.message}")
|
||||
}
|
||||
|
||||
updateNotification("Last call: #$taskId → $result")
|
||||
}
|
||||
|
||||
private suspend fun pollSmsTask() {
|
||||
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
|
||||
val deviceId = prefs.getString("device_id", null) ?: return
|
||||
val appKey = prefs.getString("app_key", null) ?: return
|
||||
|
||||
RetrofitClient.setAppKey(appKey)
|
||||
val api = RetrofitClient.apiService
|
||||
|
||||
val task = try {
|
||||
val result = api.pendingSms(deviceId, appKey)
|
||||
addLog("SMS poll OK: taskId=${result.taskId}")
|
||||
result
|
||||
} catch (e: retrofit2.HttpException) {
|
||||
addLog("SMS poll HTTP ${e.code()}: ${e.message()}")
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
addLog("SMS poll net error: ${e.javaClass.simpleName}: ${e.message}")
|
||||
return
|
||||
}
|
||||
|
||||
val taskId = task.taskId ?: return
|
||||
val phone = task.phone ?: return
|
||||
val otp = task.otp ?: return
|
||||
|
||||
addLog("SMS task #$taskId → $phone (OTP: $otp)")
|
||||
|
||||
val result = sendSms(phone, otp)
|
||||
addLog("SMS #$taskId result: $result")
|
||||
|
||||
// Report result back to server
|
||||
try {
|
||||
api.smsDone(
|
||||
CallDoneRequest(
|
||||
taskId = taskId,
|
||||
deviceId = deviceId,
|
||||
appKey = appKey,
|
||||
result = result
|
||||
)
|
||||
)
|
||||
addLog("SMS #$taskId reported: $result")
|
||||
} catch (e: Exception) {
|
||||
addLog("SMS #$taskId report failed: ${e.message}")
|
||||
}
|
||||
|
||||
updateNotification("Last SMS: #$taskId → $result")
|
||||
}
|
||||
|
||||
private fun sendSms(phone: String, otp: String): String {
|
||||
return try {
|
||||
val message = "رمز التحقق في تطبيق انطلق هو: $otp"
|
||||
|
||||
// Format phone to local Jordan format if needed (+962 -> 0)
|
||||
val formattedPhone = if (phone.startsWith("+962")) {
|
||||
val suffix = phone.substring(4)
|
||||
if (suffix.startsWith("0")) suffix else "0$suffix"
|
||||
} else {
|
||||
phone
|
||||
}
|
||||
|
||||
addLog("SMS sending to $formattedPhone...")
|
||||
|
||||
// Create a PendingIntent to track if SMS was sent (Explicit intent for Android 14+)
|
||||
val sentIntent = android.app.PendingIntent.getBroadcast(
|
||||
this, 0,
|
||||
Intent("SMS_SENT").apply { setPackage(packageName) },
|
||||
android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
getSystemService(SmsManager::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
SmsManager.getDefault()
|
||||
}
|
||||
|
||||
smsManager.sendTextMessage(formattedPhone, null, message, sentIntent, null)
|
||||
addLog("SMS handed to OS for $formattedPhone")
|
||||
"success"
|
||||
} catch (e: SecurityException) {
|
||||
addLog("SMS SecurityException: ${e.message}")
|
||||
"failed"
|
||||
} catch (e: Exception) {
|
||||
addLog("SMS Exception: ${e.javaClass.simpleName}: ${e.message}")
|
||||
"failed"
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Flash OTP Caller Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Keeps the flash call service running"
|
||||
setShowBadge(false)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(text: String): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Flash OTP Caller")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(android.R.drawable.ic_menu_call)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateNotification(text: String) {
|
||||
try {
|
||||
val notification = buildNotification(text)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
} catch (e: Exception) {
|
||||
// Ignore notification update errors
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLog(message: String) {
|
||||
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||
val logLine = "[$timestamp] $message"
|
||||
|
||||
// Also log to system Logcat for Android Studio visibility
|
||||
android.util.Log.d("CallerService", logLine)
|
||||
|
||||
// Save to SharedPreferences for MainActivity to read
|
||||
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
|
||||
val existingLogs = prefs.getString("service_logs", "") ?: ""
|
||||
val logs = (existingLogs.split("\n") + logLine).takeLast(20)
|
||||
prefs.edit().putString("service_logs", logs.joinToString("\n")).apply()
|
||||
|
||||
// Notify listener if attached
|
||||
logListener?.invoke(logLine)
|
||||
}
|
||||
|
||||
private val smsReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == "SMS_SENT") {
|
||||
val result = when (resultCode) {
|
||||
android.app.Activity.RESULT_OK -> "SUCCESS (Hardware confirmed)"
|
||||
android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE -> "FAILED (Generic/No Credit)"
|
||||
android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE -> "FAILED (No Cell Service)"
|
||||
android.telephony.SmsManager.RESULT_ERROR_NULL_PDU -> "FAILED (Null PDU)"
|
||||
android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF -> "FAILED (Radio Off/Airplane Mode)"
|
||||
else -> "FAILED (Unknown code: $resultCode)"
|
||||
}
|
||||
addLog("SMS Hardware Result: $result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.telecom.TelecomManager
|
||||
import android.telephony.TelephonyManager
|
||||
import kotlinx.coroutines.delay
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class FlashCallManager(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Make a flash call: dial the number, wait 2500ms, then hang up.
|
||||
* Returns result: "success", "failed", "busy", "no_answer"
|
||||
*/
|
||||
suspend fun makeFlashCall(phone: String): String {
|
||||
return try {
|
||||
// Place the call
|
||||
android.util.Log.d("CallerService", "Initiating flash call to $phone")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
telecomManager.placeCall(Uri.parse("tel:$phone"), null)
|
||||
} else {
|
||||
val callIntent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$phone")).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(callIntent)
|
||||
}
|
||||
|
||||
// Wait 1000ms — enough for 1 quick ring
|
||||
delay(1000)
|
||||
|
||||
// Hang up the call
|
||||
val hungUp = endCall()
|
||||
if (hungUp) "success" else "failed"
|
||||
} catch (e: SecurityException) {
|
||||
"failed"
|
||||
} catch (e: Exception) {
|
||||
"failed"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current active call.
|
||||
* Primary: TelecomManager (Android 9+)
|
||||
* Fallback: Reflection on TelephonyManager.endCall() (Android 8)
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun endCall(): Boolean {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Android 9+: Use TelecomManager
|
||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
telecomManager.endCall()
|
||||
} else {
|
||||
// Android 8: Use reflection on TelephonyManager
|
||||
endCallReflection()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Try reflection as fallback
|
||||
endCallReflection()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("PrivateApi")
|
||||
private fun endCallReflection(): Boolean {
|
||||
return try {
|
||||
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
val method: Method = TelephonyManager::class.java.getDeclaredMethod("endCall")
|
||||
method.invoke(telephonyManager) as? Boolean ?: false
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var etDeviceId: EditText
|
||||
private lateinit var etPhoneNumber: EditText
|
||||
private lateinit var etAppKey: EditText
|
||||
private lateinit var spinnerSimSlot: Spinner
|
||||
private lateinit var btnRegister: Button
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var btnToggleService: Button
|
||||
private lateinit var tvLog: TextView
|
||||
private lateinit var logScrollView: ScrollView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
|
||||
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private val prefs by lazy { getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE) }
|
||||
|
||||
private val simSlotOptions = arrayOf("SIM 0", "SIM 1")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initViews()
|
||||
loadSavedData()
|
||||
setupSpinner()
|
||||
setupClickListeners()
|
||||
requestPermissionsIfNeeded()
|
||||
updateServiceStatus()
|
||||
loadServiceLogs()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateServiceStatus()
|
||||
loadServiceLogs()
|
||||
|
||||
// Attach log listener
|
||||
CallerService.logListener = { logLine ->
|
||||
runOnUiThread {
|
||||
appendLogLine(logLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
CallerService.logListener = null
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
etDeviceId = findViewById(R.id.etDeviceId)
|
||||
etPhoneNumber = findViewById(R.id.etPhoneNumber)
|
||||
etAppKey = findViewById(R.id.etAppKey)
|
||||
spinnerSimSlot = findViewById(R.id.spinnerSimSlot)
|
||||
btnRegister = findViewById(R.id.btnRegister)
|
||||
tvStatus = findViewById(R.id.tvStatus)
|
||||
btnToggleService = findViewById(R.id.btnToggleService)
|
||||
tvLog = findViewById(R.id.tvLog)
|
||||
logScrollView = findViewById(R.id.logScrollView)
|
||||
progressBar = findViewById(R.id.progressBar)
|
||||
}
|
||||
|
||||
private fun loadSavedData() {
|
||||
// Device ID — generate once and persist
|
||||
var deviceId = prefs.getString("device_id", null)
|
||||
if (deviceId == null) {
|
||||
deviceId = UUID.randomUUID().toString()
|
||||
prefs.edit().putString("device_id", deviceId).apply()
|
||||
}
|
||||
etDeviceId.setText(deviceId)
|
||||
etDeviceId.isEnabled = false
|
||||
etDeviceId.isFocusable = false
|
||||
|
||||
// Phone number
|
||||
val savedPhone = prefs.getString("phone_number", null)
|
||||
if (savedPhone != null) {
|
||||
etPhoneNumber.setText(savedPhone)
|
||||
}
|
||||
|
||||
// App key
|
||||
val savedAppKey = prefs.getString("app_key", null)
|
||||
if (savedAppKey != null) {
|
||||
etAppKey.setText(savedAppKey)
|
||||
}
|
||||
|
||||
// SIM slot
|
||||
val savedSimSlot = prefs.getInt("sim_slot", 0)
|
||||
spinnerSimSlot.setSelection(savedSimSlot)
|
||||
}
|
||||
|
||||
private fun setupSpinner() {
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, simSlotOptions)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinnerSimSlot.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
btnRegister.setOnClickListener {
|
||||
registerDevice()
|
||||
}
|
||||
|
||||
btnToggleService.setOnClickListener {
|
||||
if (CallerService.isRunning) {
|
||||
stopCallerService()
|
||||
} else {
|
||||
startCallerService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermissionsIfNeeded() {
|
||||
if (!PermissionHelper.areAllPermissionsGranted(this)) {
|
||||
PermissionHelper.requestAllPermissions(this)
|
||||
}
|
||||
|
||||
// Request battery optimization exemption on first launch
|
||||
val hasRequestedBattery = prefs.getBoolean("requested_battery_opt", false)
|
||||
if (!hasRequestedBattery) {
|
||||
PermissionHelper.requestBatteryOptimizationExemption(this)
|
||||
prefs.edit().putBoolean("requested_battery_opt", true).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDevice() {
|
||||
val phoneNumber = etPhoneNumber.text.toString().trim()
|
||||
val appKey = etAppKey.text.toString().trim()
|
||||
val deviceId = etDeviceId.text.toString().trim()
|
||||
val simSlot = spinnerSimSlot.selectedItemPosition
|
||||
|
||||
if (phoneNumber.isEmpty()) {
|
||||
etPhoneNumber.error = "Phone number is required"
|
||||
return
|
||||
}
|
||||
|
||||
if (appKey.isEmpty()) {
|
||||
etAppKey.error = "App key is required"
|
||||
return
|
||||
}
|
||||
|
||||
if (!PermissionHelper.areAllPermissionsGranted(this)) {
|
||||
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
|
||||
PermissionHelper.requestAllPermissions(this)
|
||||
return
|
||||
}
|
||||
|
||||
btnRegister.isEnabled = false
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
mainScope.launch {
|
||||
try {
|
||||
RetrofitClient.setAppKey(appKey)
|
||||
val response = RetrofitClient.apiService.registerDevice(
|
||||
RegisterRequest(
|
||||
deviceId = deviceId,
|
||||
phoneNumber = phoneNumber,
|
||||
simSlot = simSlot,
|
||||
appKey = appKey
|
||||
)
|
||||
)
|
||||
|
||||
if (response.success) {
|
||||
// Save registration data
|
||||
prefs.edit()
|
||||
.putBoolean("is_registered", true)
|
||||
.putString("phone_number", phoneNumber)
|
||||
.putString("app_key", appKey)
|
||||
.putInt("sim_slot", simSlot)
|
||||
.apply()
|
||||
|
||||
Toast.makeText(this@MainActivity, "Device registered successfully!", Toast.LENGTH_SHORT).show()
|
||||
appendLogLine("Device registered: $deviceId")
|
||||
} else {
|
||||
val message = response.message ?: "Registration failed"
|
||||
Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
|
||||
appendLogLine("Registration failed: $message")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this@MainActivity, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
appendLogLine("Registration error: ${e.message}")
|
||||
} finally {
|
||||
btnRegister.isEnabled = true
|
||||
progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCallerService() {
|
||||
if (!PermissionHelper.areAllPermissionsGranted(this)) {
|
||||
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
|
||||
PermissionHelper.requestAllPermissions(this)
|
||||
return
|
||||
}
|
||||
|
||||
val isRegistered = prefs.getBoolean("is_registered", false)
|
||||
if (!isRegistered) {
|
||||
Toast.makeText(this, "Please register the device first", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val serviceIntent = Intent(this, CallerService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
|
||||
// Update UI immediately (optimistic)
|
||||
tvStatus.text = "Service Starting... ⏳"
|
||||
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
|
||||
btnToggleService.text = "Stop Service"
|
||||
|
||||
appendLogLine("Service start requested")
|
||||
}
|
||||
|
||||
private fun stopCallerService() {
|
||||
val serviceIntent = Intent(this, CallerService::class.java)
|
||||
stopService(serviceIntent)
|
||||
|
||||
// Update UI immediately (optimistic)
|
||||
tvStatus.text = "Service Stopping... ⏳"
|
||||
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
|
||||
btnToggleService.text = "Start Service"
|
||||
|
||||
appendLogLine("Service stop requested")
|
||||
}
|
||||
|
||||
private fun updateServiceStatus() {
|
||||
if (CallerService.isRunning) {
|
||||
tvStatus.text = "Service Running ✅"
|
||||
tvStatus.setTextColor(getColor(android.R.color.holo_green_dark))
|
||||
btnToggleService.text = "Stop Service"
|
||||
} else {
|
||||
tvStatus.text = "Service Stopped ❌"
|
||||
tvStatus.setTextColor(getColor(android.R.color.holo_red_dark))
|
||||
btnToggleService.text = "Start Service"
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadServiceLogs() {
|
||||
val logs = prefs.getString("service_logs", "") ?: ""
|
||||
tvLog.text = logs
|
||||
logScrollView.post {
|
||||
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendLogLine(line: String) {
|
||||
val current = tvLog.text.toString()
|
||||
val lines = (current.split("\n") + line).takeLast(20)
|
||||
tvLog.text = lines.joinToString("\n")
|
||||
logScrollView.post {
|
||||
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == PermissionHelper.REQUEST_CODE_PERMISSIONS) {
|
||||
val allGranted = grantResults.all { it == android.content.pm.PackageManager.PERMISSION_GRANTED }
|
||||
if (!allGranted) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Some permissions were denied. The app may not function correctly.",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
// Show which permissions are missing
|
||||
val denied = permissions.filterIndexed { index, _ ->
|
||||
grantResults[index] != android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (denied.isNotEmpty()) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Permissions Required")
|
||||
.setMessage("The following permissions are required for the app to work:\n\n" +
|
||||
denied.joinToString("\n") { perm ->
|
||||
when (perm) {
|
||||
android.Manifest.permission.CALL_PHONE -> "• Phone - to make flash calls"
|
||||
android.Manifest.permission.READ_PHONE_STATE -> "• Phone State - to read call status"
|
||||
android.Manifest.permission.SEND_SMS -> "• SMS - to send OTP messages"
|
||||
else -> "• $perm"
|
||||
}
|
||||
} +
|
||||
"\n\nPlease grant them in Settings.")
|
||||
.setPositiveButton("Open Settings") { _, _ ->
|
||||
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = android.net.Uri.fromParts("package", packageName, null)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object PermissionHelper {
|
||||
|
||||
private val REQUIRED_PERMISSIONS = arrayOf(
|
||||
Manifest.permission.CALL_PHONE,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
Manifest.permission.SEND_SMS
|
||||
)
|
||||
|
||||
fun areAllPermissionsGranted(context: Context): Boolean {
|
||||
return REQUIRED_PERMISSIONS.all { permission ->
|
||||
ContextCompat.checkSelfPermission(context, permission) ==
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAllPermissions(activity: Activity) {
|
||||
val permissionsToRequest = REQUIRED_PERMISSIONS.filter { permission ->
|
||||
ContextCompat.checkSelfPermission(activity, permission) !=
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (permissionsToRequest.isNotEmpty()) {
|
||||
// Show rationale for any permission that was previously denied
|
||||
val shouldShowRationale = permissionsToRequest.any { permission ->
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||
}
|
||||
|
||||
if (shouldShowRationale) {
|
||||
// In a production app you'd show a dialog here explaining why
|
||||
// For now, just request the permissions directly
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
permissionsToRequest.toTypedArray(),
|
||||
REQUEST_CODE_PERMISSIONS
|
||||
)
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
permissionsToRequest.toTypedArray(),
|
||||
REQUEST_CODE_PERMISSIONS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Also request battery optimization exemption
|
||||
requestBatteryOptimizationExemption(activity)
|
||||
|
||||
// Request SYSTEM_ALERT_WINDOW (Draw over other apps) for Android 10+ background activity starts
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) {
|
||||
try {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${activity.packageName}")
|
||||
)
|
||||
activity.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Ignore if not available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestBatteryOptimizationExemption(activity: Activity) {
|
||||
val powerManager = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val packageName = activity.packageName
|
||||
|
||||
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Fallback: open battery optimization settings
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
|
||||
activity.startActivity(intent)
|
||||
} catch (e2: Exception) {
|
||||
// Cannot open settings, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldShowCallPhoneRationale(activity: Activity): Boolean {
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
activity,
|
||||
Manifest.permission.CALL_PHONE
|
||||
)
|
||||
}
|
||||
|
||||
fun shouldShowReadPhoneStateRationale(activity: Activity): Boolean {
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
activity,
|
||||
Manifest.permission.READ_PHONE_STATE
|
||||
)
|
||||
}
|
||||
|
||||
fun shouldShowSendSmsRationale(activity: Activity): Boolean {
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
activity,
|
||||
Manifest.permission.SEND_SMS
|
||||
)
|
||||
}
|
||||
|
||||
const val REQUEST_CODE_PERMISSIONS = 1001
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.intaleq.flashcall
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object RetrofitClient {
|
||||
|
||||
private const val BASE_URL = "https://otp.intaleqapp.com/api/"
|
||||
|
||||
private var appKey: String = ""
|
||||
|
||||
fun setAppKey(key: String) {
|
||||
appKey = key
|
||||
}
|
||||
|
||||
private val authInterceptor = Interceptor { chain ->
|
||||
val original = chain.request()
|
||||
val request = original.newBuilder()
|
||||
.header("X-App-Key", appKey)
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
private val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
val apiService: ApiService by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(ApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,31 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="49.59793"
|
||||
android:startX="42.9492"
|
||||
android:endY="92.4963"
|
||||
android:endX="85.84757"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
220
caller-app/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:background="#FAFAFA">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Device Setup Section -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:elevation="4dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardBackgroundColor="#FFFFFF">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Device Setup"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#333333"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Device ID">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etDeviceId"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:focusable="false"
|
||||
android:textSize="12sp"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Phone Number">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPhoneNumber"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="phone"
|
||||
android:textSize="14sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Server App Key">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAppKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:textSize="14sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="SIM Slot:"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#666666"
|
||||
android:layout_marginEnd="12dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerSimSlot"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:spinnerMode="dropdown" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRegister"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="Register Device"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="15sp"
|
||||
android:backgroundTint="#4CAF50"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Service Control Section -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:elevation="4dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardBackgroundColor="#FFFFFF">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Service Control"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#333333"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Service Stopped ❌"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#F44336"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnToggleService"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="Start Service"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="15sp"
|
||||
android:backgroundTint="#4CAF50"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Live Log Section -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardBackgroundColor="#FFFFFF">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Live Log"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#333333"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/logScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:background="#F5F5F5"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#555555"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
caller-app/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
caller-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
caller-app/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
caller-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
caller-app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
caller-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
caller-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
caller-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
caller-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
18
caller-app/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#4CAF50</color>
|
||||
<color name="primary_dark">#388E3C</color>
|
||||
<color name="primary_light">#C8E6C9</color>
|
||||
<color name="accent">#4CAF50</color>
|
||||
<color name="text_primary">#333333</color>
|
||||
<color name="text_secondary">#666666</color>
|
||||
<color name="text_hint">#999999</color>
|
||||
<color name="background">#FAFAFA</color>
|
||||
<color name="card_background">#FFFFFF</color>
|
||||
<color name="success_green">#4CAF50</color>
|
||||
<color name="error_red">#F44336</color>
|
||||
<color name="log_background">#F5F5F5</color>
|
||||
<color name="log_text">#555555</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="black">#000000</color>
|
||||
</resources>
|
||||
23
caller-app/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Flash OTP Caller</string>
|
||||
<string name="device_setup">Device Setup</string>
|
||||
<string name="service_control">Service Control</string>
|
||||
<string name="device_id_hint">Device ID</string>
|
||||
<string name="phone_number_hint">Phone Number</string>
|
||||
<string name="app_key_hint">Server App Key</string>
|
||||
<string name="sim_slot_label">SIM Slot</string>
|
||||
<string name="register_button">Register Device</string>
|
||||
<string name="start_service">Start Service</string>
|
||||
<string name="stop_service">Stop Service</string>
|
||||
<string name="service_running">Service Running ✅</string>
|
||||
<string name="service_stopped">Service Stopped ❌</string>
|
||||
<string name="live_log">Live Log</string>
|
||||
<string name="permission_required">Permissions Required</string>
|
||||
<string name="permission_rationale">This app requires Phone, SMS, and Phone State permissions to make flash calls and send OTP messages.</string>
|
||||
<string name="registration_success">Device registered successfully!</string>
|
||||
<string name="registration_failed">Registration failed</string>
|
||||
<string name="permissions_denied">Some permissions were denied. The app may not function correctly.</string>
|
||||
<string name="register_first">Please register the device first</string>
|
||||
<string name="grant_permissions_first">Please grant all permissions first</string>
|
||||
</resources>
|
||||
15
caller-app/build.gradle
Normal file
@@ -0,0 +1,15 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.22'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('clean', Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
10
caller-app/gradle.properties
Normal file
@@ -0,0 +1,10 @@
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
# AndroidX Support
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
5
caller-app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
18
caller-app/settings.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "FlashOTPCaller"
|
||||
include ':app'
|
||||
36
deploy.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- Configuration ---
|
||||
GIT_BRANCH="main"
|
||||
|
||||
# Colors for terminal styling
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}🚀 Starting Flash Call OTP Sync & Push...${NC}"
|
||||
|
||||
# 1. Commit local changes automatically (with date/time or custom message)
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
COMMIT_MSG=${1:-"Deploy: $(date '+%Y-%m-%d %H:%M:%S')"}
|
||||
echo -e "${YELLOW}Staging and committing changes with message: ${COMMIT_MSG}${NC}"
|
||||
git add .
|
||||
git commit -m "$COMMIT_MSG"
|
||||
echo -e "${GREEN}✅ Local commit created successfully!${NC}"
|
||||
else
|
||||
echo -e "ℹ️ No local changes to commit."
|
||||
fi
|
||||
|
||||
# 2. Push to Git Remote
|
||||
echo -e "${GREEN}📤 Pushing changes to remote repository (${GIT_BRANCH})...${NC}"
|
||||
git push origin "$GIT_BRANCH"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}❌ Git Push failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}==========================================${NC}"
|
||||
echo -e "✨ Flash Call OTP changes pushed to Git successfully! "
|
||||
echo -e "==========================================${NC}"
|
||||
289
docs/PERMISSIONS.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Flash Call OTP System — Permissions Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Android Caller App Permissions](#1-android-caller-app-permissions)
|
||||
2. [Flutter Receiver App Permissions](#2-flutter-receiver-app-permissions)
|
||||
3. [Server-Side Security](#3-server-side-security)
|
||||
4. [iOS-Specific Notes](#4-ios-specific-notes)
|
||||
5. [Permission Request Flow](#5-permission-request-flow)
|
||||
|
||||
---
|
||||
|
||||
## 1. Android Caller App Permissions
|
||||
|
||||
### CALL_PHONE
|
||||
- **Permission String**: `android.permission.CALL_PHONE`
|
||||
- **Protection Level**: Dangerous
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Required to programmatically place phone calls without user interaction. The app uses `ACTION_CALL` with `Uri.parse("tel:$phone")` to automatically dial the target number and hang up after 2.5 seconds.
|
||||
- **Why Not ACTION_DIAL**: `ACTION_DIAL` only opens the dialer and requires the user to tap the call button. Flash call verification must be completely automatic — no user interaction.
|
||||
- **Android Version Notes**:
|
||||
- API 26-27 (Android 8.0-8.1): Standard dangerous permission, requested at runtime
|
||||
- API 28+ (Android 9+): Works as standard dangerous permission
|
||||
- If denied: The app cannot make flash calls. The service will report "failed" for all call tasks.
|
||||
|
||||
### READ_PHONE_STATE
|
||||
- **Permission String**: `android.permission.READ_PHONE_STATE`
|
||||
- **Protection Level**: Dangerous
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Required to read telephony state information including call state, network type, and SIM information. Used to detect when a call is active so the app knows when to hang up.
|
||||
- **Android Version Notes**:
|
||||
- API 26-28 (Android 8-9): Standard dangerous permission
|
||||
- API 29+ (Android 10+): More restricted; the app only needs this for call state detection
|
||||
- If denied: Call state detection may not work, but the 2.5-second timer still hangs up reliably
|
||||
|
||||
### SEND_SMS
|
||||
- **Permission String**: `android.permission.SEND_SMS`
|
||||
- **Protection Level**: Dangerous
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Required to send SMS messages containing OTP codes to iOS users. When the backend has an SMS delivery task, the Caller App sends the OTP via SMS using `SmsManager`.
|
||||
- **Why Needed**: iOS devices cannot receive flash calls for automatic OTP extraction. The system falls back to SMS delivery, and the Caller Android device acts as the SMS sender.
|
||||
- **Android Version Notes**:
|
||||
- API 26-30 (Android 8-11): Uses `SmsManager.getDefault()`
|
||||
- API 31+ (Android 12+): Uses `context.getSystemService(SmsManager::class.java)` for per-subscription SMS handling
|
||||
- If denied: SMS tasks will fail. Flash calls for Android devices still work.
|
||||
|
||||
### FOREGROUND_SERVICE
|
||||
- **Permission String**: `android.permission.FOREGROUND_SERVICE`
|
||||
- **Protection Level**: Normal
|
||||
- **Required Since**: API 26 (Android 8.0)
|
||||
- **Purpose**: Required to run a foreground service that continuously polls the backend for pending tasks. The service must be a foreground service (not a background service) to avoid being killed by the system.
|
||||
- **Android Version Notes**:
|
||||
- API 26+ (Android 8.0+): Mandatory for any long-running service
|
||||
- Without this: The service would be killed within a few minutes by the system
|
||||
|
||||
### FOREGROUND_SERVICE_PHONE_CALL
|
||||
- **Permission String**: `android.permission.FOREGROUND_SERVICE_PHONE_CALL`
|
||||
- **Protection Level**: Normal
|
||||
- **Required Since**: API 34 (Android 14)
|
||||
- **Purpose**: Specifies the foreground service type as "phone call". Required on Android 14+ for services that place phone calls.
|
||||
- **Android Version Notes**:
|
||||
- API 34+ (Android 14): Must be declared in manifest AND specified in `startForeground()` call
|
||||
- API 26-33: Not required, but harmless to declare
|
||||
- Without this on Android 14: The foreground service will crash with SecurityException
|
||||
|
||||
### RECEIVE_BOOT_COMPLETED
|
||||
- **Permission String**: `android.permission.RECEIVE_BOOT_COMPLETED`
|
||||
- **Protection Level**: Normal
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Allows the app to receive the `BOOT_COMPLETED` broadcast and automatically restart the CallerService after the device reboots. Without this, the user would have to manually open the app and start the service after every restart.
|
||||
- **Android Version Notes**:
|
||||
- All versions: Works as a normal permission (auto-granted)
|
||||
- Note: Some OEMs (Xiaomi, Huawei, Oppo) restrict auto-start by default. Users must enable "Auto-start" in security settings.
|
||||
|
||||
### REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
- **Permission String**: `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`
|
||||
- **Protection Level**: Normal
|
||||
- **Required Since**: API 23 (Android 6.0)
|
||||
- **Purpose**: Prompts the user to add the app to the battery optimization whitelist. This is critical because Android's Doze mode and app standby can kill the foreground service, preventing the device from receiving OTP tasks.
|
||||
- **How It Works**: The app sends an `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` intent with the app's package URI. The system shows a dialog asking the user to allow the app to ignore battery optimization.
|
||||
- **Android Version Notes**:
|
||||
- API 23+ (Android 6.0+): Works as normal permission with system dialog
|
||||
- Some OEMs: May require additional steps (e.g., Xiaomi: Security → Autostart; Samsung: Device Care → Battery → App Power Management)
|
||||
- Without this: The service may be killed after 10-30 minutes of screen-off time
|
||||
|
||||
### INTERNET
|
||||
- **Permission String**: `android.permission.INTERNET`
|
||||
- **Protection Level**: Normal
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Required for all network communication — polling the backend API and reporting call/SMS results.
|
||||
- **Note**: Automatically granted on all Android versions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Flutter Receiver App Permissions
|
||||
|
||||
### READ_CALL_LOG (Android Only)
|
||||
- **Permission String**: `android.permission.READ_CALL_LOG`
|
||||
- **Protection Level**: Dangerous
|
||||
- **Required Since**: API 16
|
||||
- **Purpose**: Required to read the device's call log and detect incoming missed calls from the flash call. The app queries the call log every 1.5 seconds looking for missed calls since the OTP was requested.
|
||||
- **How OTP Extraction Works**: When a flash call comes in, it appears as a missed call in the call log. The app reads the caller's phone number (e.g., +962791234829) and extracts the last 4 digits (4829) as the OTP code.
|
||||
- **Android Version Notes**:
|
||||
- API 16-22 (Android 4.1-5.1): Granted at install time
|
||||
- API 23+ (Android 6.0+): Must be requested at runtime
|
||||
- If denied: Automatic OTP detection will not work. User must manually enter the OTP code via the fallback input.
|
||||
|
||||
### READ_PHONE_STATE (Android Only)
|
||||
- **Permission String**: `android.permission.READ_PHONE_STATE`
|
||||
- **Protection Level**: Dangerous
|
||||
- **Required Since**: API 1
|
||||
- **Purpose**: Companion permission for READ_CALL_LOG. Some Android versions require both permissions to properly access call log data.
|
||||
- **Android Version Notes**:
|
||||
- API 29+ (Android 10+): More restricted; may need to request both READ_CALL_LOG and READ_PHONE_STATE together
|
||||
- If denied: Call log access may fail on some devices
|
||||
|
||||
### No Extra Permissions on iOS
|
||||
- iOS does not allow reading call logs or SMS messages programmatically
|
||||
- SMS AutoFill works automatically through `AutofillHints.oneTimeCode` without any special permissions
|
||||
- The only requirement is that the SMS follows Apple's format with `@domain #code` at the end
|
||||
|
||||
---
|
||||
|
||||
## 3. Server-Side Security
|
||||
|
||||
### API Key Authentication
|
||||
- **APP_KEY**: Used by the Flutter Receiver App for `request-otp` and `verify-otp` endpoints
|
||||
- **DEVICE_KEY**: Used by the Android Caller App for `pending-call`, `pending-sms`, `call-done`, `sms-done`, and `register-device` endpoints
|
||||
- **Implementation**: Uses `hash_equals()` for timing-safe comparison to prevent timing attacks
|
||||
- **Key Generation**: Use `openssl rand -hex 32` to generate cryptographically secure keys
|
||||
|
||||
### Rate Limiting
|
||||
- **Per-Phone Limit**: Max 3 OTP requests per phone number per 10-minute window
|
||||
- **Per-IP Limit**: Max 30 requests per IP per minute for `request-otp`
|
||||
- **General API**: Max 60 requests per IP per minute across all endpoints
|
||||
- **Implementation**: Redis-based with atomic increment operations and TTL-based expiration
|
||||
|
||||
### Input Validation
|
||||
- **Phone Format**: E.164 format validation (e.g., +9627XXXXXXXX) — regex: `/^\+[1-9]\d{6,14}$/`
|
||||
- **OTP Format**: Exactly 4 digits — regex: `/^\d{4}$/`
|
||||
- **Device ID**: Length between 5-50 characters
|
||||
- **SQL Injection**: All queries use PDO prepared statements
|
||||
- **XSS**: JSON-only responses (no HTML rendering)
|
||||
|
||||
### Data Protection
|
||||
- OTP codes stored in Redis with 120-second TTL (auto-expire)
|
||||
- OTP codes in MySQL are for audit purposes only
|
||||
- Failed verification attempts tracked in Redis (max 5 attempts)
|
||||
- Request logging masks sensitive fields (app_key, otp, password)
|
||||
- API logs auto-cleaned after 30 days
|
||||
|
||||
---
|
||||
|
||||
## 4. iOS-Specific Notes
|
||||
|
||||
### Why Flash Call Doesn't Work on iOS
|
||||
Apple restricts access to the call log through `CoreTelephony` framework. There is no public API to:
|
||||
- Read incoming/outgoing call history
|
||||
- Get caller ID from recent calls programmatically
|
||||
- Detect missed calls in real-time
|
||||
|
||||
This is a deliberate privacy decision by Apple. The workaround is to use SMS delivery with AutoFill.
|
||||
|
||||
### SMS AutoFill Mechanism
|
||||
iOS can automatically suggest OTP codes from SMS messages in the keyboard suggestion bar. This requires:
|
||||
1. **Correct SMS format**: The message must include the domain and code at the end:
|
||||
```
|
||||
رمز التحقق في Intaleq هو 4829
|
||||
لا تشاركه مع أحد.
|
||||
|
||||
@otp.intaleqapp.com #4829
|
||||
```
|
||||
2. **Associated Domains**: The app must have `otp.intaleqapp.com` as an associated domain in its entitlements (for the `#code` to be recognized)
|
||||
3. **Text field configuration**: The OTP input must have `autofillHints: [AutofillHints.oneTimeCode]` and `keyboardType: TextInputType.number`
|
||||
|
||||
### iOS AutoFill Requirements
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| SMS format | Must end with `@domain #code` |
|
||||
| Associated domain | Configured in Xcode → Signing & Capabilities |
|
||||
| Text field | `AutofillHints.oneTimeCode` + number keyboard |
|
||||
| Real device | AutoFill does not work on simulators |
|
||||
| SMS sender | Must come from a phone number (not alphanumeric) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Permission Request Flow
|
||||
|
||||
### Android Caller App — First Launch
|
||||
|
||||
```
|
||||
App Launched
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ PermissionHelper │
|
||||
│ .requestAllPermissions()│
|
||||
└──────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ CALL_PHONE │──── Denied ──→ Show rationale dialog
|
||||
│ │ │
|
||||
│ │──── Granted ──→ Continue
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│READ_PHONE_STATE│── Denied ──→ Show rationale dialog
|
||||
│ │ │
|
||||
│ │── Granted ──→ Continue
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ SEND_SMS │──── Denied ──→ Show rationale dialog
|
||||
│ │ │
|
||||
│ │──── Granted ──→ Continue
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ Battery Optimization Exemption │
|
||||
│ (Intent to system settings) │
|
||||
│ │
|
||||
│ User selects "Don't optimize" │
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Ready for Registration
|
||||
```
|
||||
|
||||
### Flutter Receiver App — OTP Flow
|
||||
|
||||
```
|
||||
User taps "Send OTP"
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Platform check │
|
||||
└──────┬──────────────────┘
|
||||
│
|
||||
├──── Android ──→ Check READ_CALL_LOG permission
|
||||
│ │
|
||||
│ ├── Granted ──→ Start call log polling
|
||||
│ │
|
||||
│ ├── Denied ──→ Show rationale:
|
||||
│ │ "نحتاج صلاحية قراءة سجل المكالمات
|
||||
│ │ للتحقق التلقائي دون الحاجة لإدخال أي كود"
|
||||
│ │
|
||||
│ ├── Permanently Denied ──→ Redirect to Settings
|
||||
│ │ + Show manual OTP input fallback
|
||||
│ │
|
||||
│ └── Still Denied ──→ Show manual OTP input
|
||||
│
|
||||
└──── iOS ──→ Skip call log entirely
|
||||
│
|
||||
Show manual 4-digit input
|
||||
with AutofillHints.oneTimeCode
|
||||
│
|
||||
iOS keyboard shows SMS code
|
||||
suggestion automatically
|
||||
```
|
||||
|
||||
### Arabic Permission Explanations
|
||||
|
||||
| Permission | Arabic Explanation |
|
||||
|-----------|-------------------|
|
||||
| CALL_PHONE | "صلاحية إجراء المكالمات مطلوبة للاتصال التلقائي بأرقام التحقق" |
|
||||
| READ_PHONE_STATE | "صلاحية قراءة حالة الهاتف مطلوبة لإدارة المكالمات" |
|
||||
| SEND_SMS | "صلاحية إرسال الرسائل مطلوبة للتحقق من مستخدمي iOS" |
|
||||
| READ_CALL_LOG | "نحتاج صلاحية قراءة سجل المكالمات للتحقق التلقائي دون الحاجة لإدخال أي كود" |
|
||||
| Battery Optimization | "يجب تعطيل تحسين البطارية لضمان عمل الخدمة بشكل مستمر" |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Permission Summary by Component
|
||||
|
||||
| Permission | Caller App | Receiver App | Type | Min API |
|
||||
|-----------|:----------:|:------------:|------|---------|
|
||||
| CALL_PHONE | ✅ | ❌ | Dangerous | 1 |
|
||||
| READ_PHONE_STATE | ✅ | ✅ | Dangerous | 1 |
|
||||
| SEND_SMS | ✅ | ❌ | Dangerous | 1 |
|
||||
| READ_CALL_LOG | ❌ | ✅ | Dangerous | 16 |
|
||||
| FOREGROUND_SERVICE | ✅ | ❌ | Normal | 26 |
|
||||
| FOREGROUND_SERVICE_PHONE_CALL | ✅ | ❌ | Normal | 34 |
|
||||
| RECEIVE_BOOT_COMPLETED | ✅ | ❌ | Normal | 1 |
|
||||
| REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | ✅ | ❌ | Normal | 23 |
|
||||
| INTERNET | ✅ | ✅ | Normal | 1 |
|
||||
497
docs/SETUP.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# Flash Call OTP System — Setup Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#1-prerequisites)
|
||||
2. [Server Setup (PHP Backend)](#2-server-setup-php-backend)
|
||||
3. [Android Caller App Setup](#3-android-caller-app-setup)
|
||||
4. [Flutter Receiver App Setup](#4-flutter-receiver-app-setup)
|
||||
5. [End-to-End Testing](#5-end-to-end-testing)
|
||||
6. [Troubleshooting](#6-troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
|
||||
- Ubuntu 20.04+ or similar Linux distribution
|
||||
- PHP 8.1+ with phpredis extension
|
||||
- MySQL 8.0+ or MariaDB 10.6+
|
||||
- Redis 6.0+
|
||||
- Nginx 1.18+
|
||||
- Certbot (Let's Encrypt) for SSL
|
||||
- Composer (optional, for dependency management)
|
||||
|
||||
### Android Caller App Requirements
|
||||
|
||||
- Android Studio Hedgehog (2023.1.1) or newer
|
||||
- Android device running Android 8.0 (API 26) or higher
|
||||
- Active SIM card installed in the device
|
||||
- Stable internet connection (Wi-Fi recommended)
|
||||
|
||||
### Flutter Receiver App Requirements
|
||||
|
||||
- Flutter 3.16+ with Dart 3.x
|
||||
- Android device for full auto-read testing
|
||||
- iOS device for SMS AutoFill testing (optional)
|
||||
- Xcode 15+ (for iOS builds)
|
||||
|
||||
---
|
||||
|
||||
## 2. Server Setup (PHP Backend)
|
||||
|
||||
### 2.1 Install Required Packages
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y nginx php8.1-fpm php8.1-mysql php8.1-redis php8.1-mbstring php8.1-xml php8.1-curl mysql-server redis-server
|
||||
```
|
||||
|
||||
### 2.2 Configure MySQL
|
||||
|
||||
```bash
|
||||
sudo mysql_secure_installation
|
||||
```
|
||||
|
||||
Log in to MySQL and create the database and user:
|
||||
|
||||
```bash
|
||||
sudo mysql -u root -p
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE DATABASE otp-db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'otp-user'@'localhost' IDENTIFIED BY 'tCO1zuHWVKjTnryDDInL';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON otp_db.* TO 'otp-user'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 2.3 Import Database Schema
|
||||
|
||||
```bash
|
||||
mysql -u otp_user -p otp_db < /path/to/backend/database.sql
|
||||
```
|
||||
|
||||
Verify the tables were created:
|
||||
|
||||
```bash
|
||||
mysql -u otp_user -p otp_db -e "SHOW TABLES;"
|
||||
```
|
||||
|
||||
You should see: `api_logs`, `caller_devices`, `otp_requests`
|
||||
|
||||
### 2.4 Configure Redis
|
||||
|
||||
Edit Redis configuration for security:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/redis/redis.conf
|
||||
```
|
||||
|
||||
Set the following:
|
||||
|
||||
```
|
||||
bind 127.0.0.1
|
||||
protected-mode yes
|
||||
# requirepass YOUR_REDIS_PASSWORD # Uncomment and set if needed
|
||||
```
|
||||
|
||||
Restart Redis:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart redis-server
|
||||
sudo systemctl enable redis-server
|
||||
```
|
||||
|
||||
Test connection:
|
||||
|
||||
```bash
|
||||
redis-cli ping
|
||||
# Should return: PONG
|
||||
```
|
||||
|
||||
### 2.5 Deploy PHP Backend
|
||||
|
||||
Create the web root directory:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/otp.intaleqapp.com/public
|
||||
sudo chown -R www-data:www-data /var/www/otp.intaleqapp.com
|
||||
```
|
||||
|
||||
Copy the backend files:
|
||||
|
||||
```bash
|
||||
# Copy the entire backend directory
|
||||
cp -r backend/* /var/www/otp.intaleqapp.com/
|
||||
|
||||
# The structure should be:
|
||||
# /var/www/otp.intaleqapp.com/
|
||||
# ├── api/
|
||||
# │ ├── request-otp.php
|
||||
# │ ├── pending-call.php
|
||||
# │ ├── call-done.php
|
||||
# │ ├── verify-otp.php
|
||||
# │ ├── register-device.php
|
||||
# │ ├── pending-sms.php
|
||||
# │ └── sms-done.php
|
||||
# ├── includes/
|
||||
# │ ├── Database.php
|
||||
# │ ├── Redis.php
|
||||
# │ ├── RateLimit.php
|
||||
# │ ├── Auth.php
|
||||
# │ └── Logger.php
|
||||
# ├── config.php
|
||||
# ├── .htaccess
|
||||
# ├── database.sql
|
||||
# ├── nginx.conf
|
||||
# └── logs/
|
||||
```
|
||||
|
||||
Set permissions:
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/otp.intaleqapp.com
|
||||
sudo chmod -R 755 /var/www/otp.intaleqapp.com
|
||||
sudo chmod -R 775 /var/www/otp.intaleqapp.com/logs
|
||||
```
|
||||
|
||||
### 2.6 Configure config.php
|
||||
|
||||
Edit the configuration file with your production credentials:
|
||||
|
||||
```bash
|
||||
sudo nano /var/www/otp.intaleqapp.com/config.php
|
||||
```
|
||||
|
||||
Update these critical values:
|
||||
|
||||
```php
|
||||
define('DB_PASS', 'YOUR_ACTUAL_STRONG_PASSWORD');
|
||||
define('APP_KEY', 'GENERATE_A_SECURE_RANDOM_KEY_HERE');
|
||||
define('DEVICE_KEY', 'GENERATE_A_DIFFERENT_SECURE_KEY_HERE');
|
||||
```
|
||||
|
||||
Generate secure keys:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
### 2.7 Configure Nginx
|
||||
|
||||
Copy the nginx configuration:
|
||||
|
||||
```bash
|
||||
sudo cp /var/www/otp.intaleqapp.com/nginx.conf /etc/nginx/sites-available/otp.intaleqapp.com
|
||||
sudo ln -s /etc/nginx/sites-available/otp.intaleqapp.com /etc/nginx/sites-enabled/
|
||||
```
|
||||
|
||||
Test the configuration:
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
Reload Nginx:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 2.8 Install SSL Certificate
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d otp.intaleqapp.com
|
||||
```
|
||||
|
||||
Follow the prompts. Certbot will automatically modify the Nginx config for SSL.
|
||||
|
||||
Set up auto-renewal:
|
||||
|
||||
```bash
|
||||
sudo crontab -e
|
||||
# Add: 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
|
||||
```
|
||||
|
||||
### 2.9 Configure PHP-FPM
|
||||
|
||||
Ensure PHP-FPM is running:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable php8.1-fpm
|
||||
sudo systemctl start php8.1-fpm
|
||||
```
|
||||
|
||||
Check the socket path matches your nginx config:
|
||||
|
||||
```bash
|
||||
ls -la /var/run/php/php8.1-fpm.sock
|
||||
```
|
||||
|
||||
### 2.10 Verify the Backend
|
||||
|
||||
Test the health endpoint:
|
||||
|
||||
```bash
|
||||
curl https://otp.intaleqapp.com/health
|
||||
# Should return: {"status":"ok"}
|
||||
```
|
||||
|
||||
Test the OTP request (will fail without valid app_key, but confirms routing works):
|
||||
|
||||
```bash
|
||||
curl -X POST https://otp.intaleqapp.com/api/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"+962790000000","app_key":"wrong_key"}'
|
||||
# Should return: {"success":false,"message":"invalid_app_key"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Android Caller App Setup
|
||||
|
||||
### 3.1 Build the App
|
||||
|
||||
1. Open Android Studio
|
||||
2. Select "Open an existing project"
|
||||
3. Navigate to the `caller-app/` directory
|
||||
4. Wait for Gradle sync to complete
|
||||
5. Connect your Android device (with USB debugging enabled)
|
||||
6. Click Run ▶️
|
||||
|
||||
### 3.2 First-Time Setup on the Device
|
||||
|
||||
1. **Install the APK** on the cheap Android phone with SIM card
|
||||
2. **Grant all permissions** when prompted:
|
||||
- Phone (CALL_PHONE)
|
||||
- Phone State (READ_PHONE_STATE)
|
||||
- SMS (SEND_SMS)
|
||||
3. **Disable Battery Optimization** — the app will prompt you; select "Don't optimize" for Flash OTP Caller
|
||||
4. On the main screen:
|
||||
- **Device ID**: Auto-generated UUID (note this down)
|
||||
- **Phone Number**: Enter the phone number of the SIM in this device (e.g., +96279XXXXXXX)
|
||||
- **App Key**: Enter the DEVICE_KEY from your server's config.php
|
||||
- **SIM Slot**: Select 0 (or 1 for dual SIM)
|
||||
5. Tap **"Register Device"** — you should see a success message
|
||||
6. Tap **"Start Service"** — the notification should show "Flash OTP Caller — Active"
|
||||
|
||||
### 3.3 Verify Service is Running
|
||||
|
||||
- Check the persistent notification in the status bar
|
||||
- The status indicator should show "Service Running ✅"
|
||||
- The live log area should start showing "Polling..." messages
|
||||
- Verify on the server that the device is registered:
|
||||
|
||||
```bash
|
||||
mysql -u otp_user -p otp_db -e "SELECT * FROM caller_devices;"
|
||||
```
|
||||
|
||||
### 3.4 Keep the App Running
|
||||
|
||||
- **Lock the app** in recent apps (prevent it from being killed)
|
||||
- On Xiaomi/OPPO/Vivo: Enable "Auto-start" in security settings
|
||||
- On Samsung: Disable "Put app to sleep" in battery settings
|
||||
- Keep the phone plugged into charger if possible
|
||||
- Use a dedicated SIM card with an active plan that can make calls and send SMS
|
||||
|
||||
---
|
||||
|
||||
## 4. Flutter Receiver App Setup
|
||||
|
||||
### 4.1 Build the App
|
||||
|
||||
```bash
|
||||
cd receiver-app/
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
For APK build:
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
For iOS (macOS only):
|
||||
|
||||
```bash
|
||||
cd receiver-app/
|
||||
flutter pub get
|
||||
cd ios/
|
||||
pod install
|
||||
cd ..
|
||||
flutter run
|
||||
```
|
||||
|
||||
### 4.2 Testing on Android
|
||||
|
||||
1. Install the app on a test phone (Phone B)
|
||||
2. Open the app
|
||||
3. Enter the phone number of Phone B (with +962 prefix)
|
||||
4. Ensure "Android" is selected as device type
|
||||
5. Tap "Send OTP"
|
||||
6. Phone A (Caller device) should call Phone B within 3-6 seconds
|
||||
7. The call will come from a number like +96279XX{OTP4digits}
|
||||
8. Phone B's call log will detect the missed call
|
||||
9. The app extracts the last 4 digits as OTP
|
||||
10. Auto-verifies with the backend
|
||||
11. Success screen appears
|
||||
|
||||
### 4.3 Testing on iOS
|
||||
|
||||
1. Install the app on an iPhone
|
||||
2. Open the app
|
||||
3. Enter the phone number of the iPhone (with +962 prefix)
|
||||
4. Toggle device type to "iOS"
|
||||
5. Tap "Send OTP"
|
||||
6. The Caller Android device will send an SMS
|
||||
7. iOS keyboard will show the AutoFill suggestion
|
||||
8. Tap the suggestion (one tap only)
|
||||
9. The app auto-submits the OTP
|
||||
10. Success screen appears
|
||||
|
||||
---
|
||||
|
||||
## 5. End-to-End Testing
|
||||
|
||||
### Complete Test Flow
|
||||
|
||||
1. **Server**: Confirm health endpoint returns `{"status":"ok"}`
|
||||
2. **Caller Device**: Ensure service is running with "Service Running ✅"
|
||||
3. **Receiver App**: Enter phone number and tap "Send OTP"
|
||||
4. **Timing**: Call should arrive within 3-6 seconds
|
||||
5. **Auto-detect**: Receiver app should detect the missed call within 1-2 seconds
|
||||
6. **Verification**: Success screen should appear without any user input
|
||||
7. **Database check**: Verify the record in `otp_requests` has `verified_at` set
|
||||
|
||||
### Testing Individual API Endpoints
|
||||
|
||||
```bash
|
||||
# Request OTP
|
||||
curl -X POST https://otp.intaleqapp.com/api/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"+962790000001","app_key":"YOUR_APP_KEY","device_type":"android"}'
|
||||
|
||||
# Verify OTP
|
||||
curl -X POST https://otp.intaleqapp.com/api/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"+962790000001","otp":"1234","app_key":"YOUR_APP_KEY"}'
|
||||
|
||||
# Check pending calls (Caller App)
|
||||
curl "https://otp.intaleqapp.com/api/pending-call?device_id=DEVICE_XXX&app_key=YOUR_DEVICE_KEY"
|
||||
|
||||
# Check pending SMS (Caller App)
|
||||
curl "https://otp.intaleqapp.com/api/pending-sms?device_id=DEVICE_XXX&app_key=YOUR_DEVICE_KEY"
|
||||
|
||||
# Register device
|
||||
curl -X POST https://otp.intaleqapp.com/api/register-device \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id":"DEVICE_001","phone_number":"+962790000000","sim_slot":0,"app_key":"YOUR_DEVICE_KEY"}'
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
Check server logs:
|
||||
|
||||
```bash
|
||||
# Nginx access log
|
||||
sudo tail -f /var/log/nginx/otp.intaleqapp.com.access.log
|
||||
|
||||
# PHP error log
|
||||
sudo tail -f /var/log/php8.1-fpm/error.log
|
||||
|
||||
# Application log
|
||||
tail -f /var/www/otp.intaleqapp.com/logs/api.log
|
||||
```
|
||||
|
||||
Check Redis:
|
||||
|
||||
```bash
|
||||
redis-cli
|
||||
> KEYS otp:*
|
||||
> TTL otp:+962790000001
|
||||
> GET otp:+962790000001
|
||||
```
|
||||
|
||||
Check MySQL:
|
||||
|
||||
```sql
|
||||
-- Recent OTP requests
|
||||
SELECT * FROM otp_requests ORDER BY created_at DESC LIMIT 10;
|
||||
|
||||
-- Active devices
|
||||
SELECT * FROM caller_devices WHERE is_active = 1;
|
||||
|
||||
-- Today's stats
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM otp_requests
|
||||
WHERE DATE(created_at) = CURDATE()
|
||||
GROUP BY status;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
### Caller Device Not Receiving Tasks
|
||||
|
||||
1. Check the device is registered: `SELECT * FROM caller_devices WHERE device_id = 'YOUR_DEVICE_ID';`
|
||||
2. Check `is_active = 1` and `last_seen` is recent
|
||||
3. Check the Caller App has internet access
|
||||
4. Check the app_key matches DEVICE_KEY in config.php
|
||||
5. Restart the CallerService
|
||||
|
||||
### Calls Not Going Through
|
||||
|
||||
1. Verify CALL_PHONE permission is granted
|
||||
2. Check the SIM card has credit/plan for making calls
|
||||
3. Check the phone number format in the task
|
||||
4. Try manually dialing the number from the phone app
|
||||
5. Check if Do Not Disturb mode is off
|
||||
|
||||
### SMS Not Sending
|
||||
|
||||
1. Verify SEND_SMS permission is granted
|
||||
2. Check the SIM card can send SMS
|
||||
3. Check the destination number format
|
||||
4. Test sending a manual SMS from the device
|
||||
|
||||
### OTP Auto-detect Not Working (Flutter App)
|
||||
|
||||
1. Verify READ_CALL_LOG and READ_PHONE_STATE permissions are granted
|
||||
2. Check the phone actually received a missed call
|
||||
3. Verify the call type is detected as "missed" (not "rejected")
|
||||
4. If the user answers the call, it won't be in the missed call log
|
||||
5. Try the manual entry fallback
|
||||
|
||||
### iOS AutoFill Not Working
|
||||
|
||||
1. Ensure the SMS format includes `@otp.intaleqapp.com #{OTP}` at the end
|
||||
2. The domain must match the app's associated domain
|
||||
3. Test with a real device (simulators don't receive SMS)
|
||||
4. Check that `AutofillHints.oneTimeCode` is set on the text field
|
||||
5. The SMS must come from a phone number, not an alphanumeric sender
|
||||
|
||||
### Rate Limiting Issues
|
||||
|
||||
1. Check Redis is running: `redis-cli ping`
|
||||
2. Clear rate limit for a phone: `redis-cli DEL rate_limit:otp:+962790000001`
|
||||
3. Adjust limits in config.php if needed
|
||||
|
||||
### Database Connection Errors
|
||||
|
||||
1. Verify MySQL is running: `sudo systemctl status mysql`
|
||||
2. Check credentials in config.php
|
||||
3. Test connection: `mysql -u otp_user -p -h localhost otp_db`
|
||||
4. Check PHP-FPM error log
|
||||
|
||||
### High Server Load
|
||||
|
||||
1. Check the polling interval (3 seconds default)
|
||||
2. Consider increasing to 5 seconds if many devices are connected
|
||||
3. Use Redis for session data, MySQL for persistent storage only
|
||||
4. Monitor with `top`, `htop`, or `mysqladmin processlist`
|
||||
258
docs/SMART_OTP_GATEWAY.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# دليل تصميم وهندسة نظام بوابة التحقق الذكي (Smart OTP Gateway)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يوفر هذا المستند تقييماً تفصيلياً وتصميماً هندسياً لبوابة التحقق الذكية المقترحة لدمج قنوات وتساب وتيليجرام، مع الحفاظ على أنظمة مكالمات الفلاش والرسائل النصية كقنوات بديلة.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ١. تقييم المفهوم: تجربة المستخدم البشرية ضد أنظمة كشف الروبوتات
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يعد تحقيق التوازن بين تجربة المستخدم (UX) وفلاتر الحماية التلقائية (خصوصاً فلاتر حظر الأرقام وأنظمة التعرف البصري على النصوص في وتساب) أحد أكبر التحديات في أنظمة التحقق.
|
||||
- المبالغة في الحماية (مثل الصور المشوهة جداً) تزيد من صعوبة الاستخدام ونسبة إلغاء العملية من المستخدم.
|
||||
- تسهيل العملية بشكل مبالغ فيه (مثل إرسال الرمز كنص عادي) يزيد من نسب اكتشاف الرسائل كرسائل ترويجية أو سبام وحظر أرقام الإرسال.
|
||||
|
||||
الحل الهندسي يكمن في تطبيق "واجهة تحقق متعددة الطبقات" مدعومة بـ "شبكة تحقق ديناميكية متكيفة".
|
||||
</div>
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OTP Delivery Message │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ IMAGE LAYER │ <-- 80% Anti-Detection Entropy │
|
||||
│ │ [ 5 4 1 2 ] │ (Dynamic Generation/Reveal) │
|
||||
│ └───────────────────────┘ │
|
||||
│ │
|
||||
│ Click the button below to verify instantly: │
|
||||
│ [ Verify Now (Deep Link) ] <-- 100% Seamless UX │
|
||||
│ │
|
||||
│ *Hidden metadata container* <-- 20% OCR Noise │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
الركائز الأساسية لواجهة التحقق متعددة الطبقات:
|
||||
١. طبقة العرض المرئي الأساسي (الصور والوسائط الديناميكية): يعرض رمز التحقق داخل صورة يتم توليدها ديناميكياً مع خطوط تشويش خفيفة.
|
||||
٢. البيانات الخفية المصاحبة للرسالة: تحتوي الرسالة على الرمز مشفراً أو مخفياً باستخدام علامات ترميز يونيكود مخفية (مثل المسافات الصفرية أو علامات الاتجاه) لقراءتها تلقائياً بواسطة التطبيق دون أن تثير انتباه العين البشرية أو الروبوتات.
|
||||
٣. روابط التحقق الفورية (الروابط العميقة): تحتوي الرسالة على رابط عميق آمن يقوم بفتح التطبيق وتأكيد العملية تلقائياً عند النقر عليه دون الحاجة لكتابة أو نسخ أي رمز.
|
||||
٤. التوجيه الديناميكي المتكيف مع المخاطر: يقوم النظام بتغيير طريقة التحقق تلقائياً بناءً على مستوى خطورة الطلب.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ٢. البنية التحتية الحالية للنظام
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يتكون النظام الحالي لمكالمات الفلاش من ثلاثة أجزاء رئيسية:
|
||||
١. خادم الخلفية (PHP API): يتولى إنشاء الرموز، وإدارة الأجهزة المتصلة، وتسجيل المهام، والتحقق من الرموز.
|
||||
٢. تطبيق الاتصال (Android Caller App): يعمل على هواتف أندرويد حقيقية متصلة بالإنترنت وتحتوي على شرائح اتصال نشطة، حيث يقوم بطلب المهام المعلقة وتنفيذ المكالمات والرسائل النصية تلقائياً.
|
||||
٣. تطبيق الاستقبال (Flutter Receiver App): يثبت لدى المستخدم النهائي، ويقوم بالتحقق التلقائي في أندرويد عبر قراءة سجل المكالمات للتعرف على مكالمة الفلاش الفائتة، بينما يتيح التعبئة التلقائية للرسائل النصية في آيفون.
|
||||
</div>
|
||||
|
||||
### ٢.١ مخطط سير العمل الحالي في أندرويد (مكالمة الفلاش)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as User Device (Android)
|
||||
participant App as Flutter Receiver App
|
||||
participant BE as PHP Backend API
|
||||
participant DB as Database / Redis
|
||||
participant Caller as Android Caller Node
|
||||
|
||||
User->>App: Input Phone Number & Submit
|
||||
App->>BE: POST /api/request-otp (phone, device_type: "android")
|
||||
BE->>BE: Generate 4-digit OTP (e.g., 4829)
|
||||
BE->>DB: Store OTP in Redis (TTL: 120s)
|
||||
BE->>DB: Assign Task (round-robin) to Active Caller Node
|
||||
BE->>BE: Format Caller ID Prefix + 2 Random Digits + OTP (e.g., +96279XX4829)
|
||||
BE-->>App: JSON Response (otp_id, expires_in, method: "flash_call")
|
||||
App->>App: Request READ_CALL_LOG permission & Start Polling
|
||||
|
||||
loop Polling Tasks (Every 3s)
|
||||
Caller->>BE: GET /api/pending-call?device_id=X
|
||||
BE-->>Caller: JSON (pending call: phone + dynamic Caller ID)
|
||||
end
|
||||
|
||||
Caller->>User: Initiates Voice Call (displays +96279XX4829)
|
||||
Caller->>Caller: Wait 2.5s & Programmatically Terminate Call (Missed Call)
|
||||
Caller->>BE: POST /api/call-done (status: "success")
|
||||
|
||||
App->>App: Detects Missed Call in Call Log
|
||||
App->>App: Extracts last 4 digits of Caller ID (4829)
|
||||
App->>BE: POST /api/verify-otp (phone, otp)
|
||||
BE->>DB: Verify OTP in Redis & Update DB Status
|
||||
BE-->>App: {"success": true, "message": "verified"}
|
||||
App->>User: Display Success Screen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ٣. بنية بوابة التحقق الذكي المقترحة
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
تقوم البوابة الذكية بفحص توفر حساب للرقم على وتساب وتيليجرام أولاً لتحديد قناة الإرسال المثالية بدلاً من الاعتماد الكلي على نظام تشغيل الهاتف فقط.
|
||||
</div>
|
||||
|
||||
### مخطط اتخاذ القرار وقنوات التوجيه
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Start: Request OTP] --> B{Check WhatsApp Availability}
|
||||
B -- Exists --> C[Determine Risk Level]
|
||||
C -- Low Risk --> D1[Send Obfuscated Text OTP]
|
||||
C -- Medium Risk --> D2[Send Dynamic OTP Image + Deep Link]
|
||||
C -- High Risk --> D3[Send Short APNG/MP4 + Deep Link]
|
||||
|
||||
B -- Does Not Exist --> F{Check Telegram Availability}
|
||||
F -- Exists --> G[Send OTP via Telegram Bot/Client]
|
||||
|
||||
F -- Does Not Exist --> H{Check Device OS}
|
||||
H -- Android --> I[Trigger Flash Call via Caller Node]
|
||||
H -- iOS --> J[Trigger SMS via Caller Node]
|
||||
|
||||
D1 --> K[End: Await Verification]
|
||||
D2 --> K
|
||||
D3 --> K
|
||||
G --> K
|
||||
I --> K
|
||||
J --> K
|
||||
```
|
||||
|
||||
### ٣.١ تصنيف مستويات المخاطر الديناميكية
|
||||
|
||||
| مستوى الخطورة | القناة والمحتوى | مستوى الأمان | مستوى صعوبة تجربة المستخدم | حالات الاستخدام المستهدفة |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **المستوى الأول (منخفض)** | رمز نصي عادي (مع علامات يونيكود مخفية) | منخفض | منخفض جداً (تعبئة تلقائية) | عناوين إنترنت موثوقة، مستخدمون متكررون |
|
||||
| **المستوى الثاني (متوسط)** | صورة ديناميكية ثابتة + رابط تحقق فوري | متوسط | منخفض (التحقق بنقرة واحدة) | التسجيلات القياسية، التوجيه الافتراضي |
|
||||
| **المستوى الثالث (مرتفع)** | صورة متحركة APNG/MP4 تظهر تدريجياً + رابط تحقق | مرتفع | منخفض (التحقق بنقرة واحدة) | عناوين إنترنت مشبوهة، طلبات متكررة بكثافة |
|
||||
| **المستوى الرابع (مشبوه)** | مكالمة فلاش خارجية / رمز صوتي | أقصى حد | متوسط (إدخال يدوي وتحدي) | تخطي حدود المحاولات المسموحة |
|
||||
|
||||
---
|
||||
|
||||
## ٤. المواصفات البرمجية للتكامل
|
||||
|
||||
### ٤.١ بوابة وتساب (Node.js Baileys Gateway)
|
||||
|
||||
#### أ. إضافة واجهة التحقق من الأرقام في خادم الويب
|
||||
في ملف `server.js`:
|
||||
|
||||
```javascript
|
||||
app.post('/api/contacts/check', async (req, res) => {
|
||||
const { session_key, phone } = req.body;
|
||||
if (!session_key || !phone) {
|
||||
return res.status(400).json({ error: 'Missing session_key or phone' });
|
||||
}
|
||||
try {
|
||||
const result = await checkWhatsApp(session_key, phone);
|
||||
res.json({ status: 'success', data: result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || 'Failed to check contact' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### ب. توليد صورة رمز التحقق ورابط التحقق التلقائي (PHP Backend)
|
||||
|
||||
```php
|
||||
function generateWhatsAppOtpPayload($otp, $phone, $riskLevel) {
|
||||
// Generate Deep Link Token
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
// Store deep link token in Redis mapped to phone & OTP (expires in 120s)
|
||||
$redis = RedisClient::getInstance();
|
||||
$redis->setex("verify_token:{$token}", 120, json_encode([
|
||||
'phone' => $phone,
|
||||
'otp' => $otp
|
||||
]));
|
||||
|
||||
$deepLink = "https://verify.nabeh.app/otp?token=" . $token;
|
||||
|
||||
if ($riskLevel === 'level_1') {
|
||||
// Obfuscate OTP text using zero-width joiners (ZWJ) or LTR/RTL control characters
|
||||
$obfuscatedOtp = "\u{202D}" . implode("\u{200B}", str_split($otp)) . "\u{202C}";
|
||||
return [
|
||||
'type' => 'text',
|
||||
'message' => "رمز التحقق الخاص بك هو: " . $obfuscatedOtp . "\n\nأو اضغط للتحقق الفوري:\n" . $deepLink
|
||||
];
|
||||
}
|
||||
|
||||
// Create base64 dynamic image for Level 2 & 3
|
||||
$imageBase64 = generateOtpImageBase64($otp);
|
||||
|
||||
return [
|
||||
'type' => 'image',
|
||||
'image' => $imageBase64,
|
||||
'message' => "اضغط على الرابط أدناه لتأكيد هويتك تلقائيًا دون نسخ الرمز:\n\n" . $deepLink
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ٥. أتمتة التحقق في تطبيق الهاتف (Flutter Client)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
لتحقيق واجهة مستخدم بدون كتابة أو تعبئة يدوية للرموز، يتم تطبيق آليتين أساسيتين في تطبيق فلاتر:
|
||||
</div>
|
||||
|
||||
### ٥.١ معالجة الروابط العميقة (Deep Links)
|
||||
<div dir="rtl" align="right">
|
||||
يقوم التطبيق بتسجيل الدومين الخاص به. عند النقر على الرابط من داخل محادثة وتساب أو تيليجرام:
|
||||
١. يكتشف نظام التشغيل الرابط ويفتح تطبيق الهاتف تلقائياً.
|
||||
٢. يقوم التطبيق باستخراج التوكين وإرساله مباشرة للخلفية.
|
||||
٣. تكتمل عملية التحقق بنجاح وينتقل المستخدم للشاشة التالية فوراُ.
|
||||
</div>
|
||||
|
||||
### ٥.٢ قارئ الإشعارات التلقائي (Notification Listener - Android)
|
||||
<div dir="rtl" align="right">
|
||||
بالنسبة لهواتف أندرويد، يمكن الاستماع للإشعارات الواردة برمجياً لقراءة الرسائل وتمرير التوكين أو الرمز المخفي دون الحاجة لفتح وتساب:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
void onNotificationReceived(NotificationEvent event) {
|
||||
if (event.packageName == "com.whatsapp" && event.title == "Nabeh") {
|
||||
final body = event.text ?? "";
|
||||
final tokenRegex = RegExp(r"token=([a-f0-9]+)");
|
||||
if (tokenRegex.hasMatch(body)) {
|
||||
final token = tokenRegex.firstMatch(body)!.group(1);
|
||||
_verifyToken(token!);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ٦. تعديلات قاعدة البيانات
|
||||
|
||||
```sql
|
||||
ALTER TABLE otp_requests
|
||||
ADD COLUMN channel ENUM('flash_call', 'sms', 'whatsapp', 'telegram') NOT NULL DEFAULT 'flash_call' AFTER method,
|
||||
ADD COLUMN risk_level ENUM('level_1', 'level_2', 'level_3', 'level_4') NOT NULL DEFAULT 'level_2' AFTER channel,
|
||||
ADD COLUMN deep_link_token VARCHAR(64) NULL DEFAULT NULL AFTER risk_level;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ٧. خطة التطوير والتشغيل
|
||||
|
||||
### المرحلة الأولى: تهيئة قنوات الفحص والروابط العميقة
|
||||
<div dir="rtl" align="right">
|
||||
١. كتابة وتفعيل منطق الروابط العميقة (Deep Links) في خادم PHP وتطبيق فلاتر.
|
||||
٢. ربط النطاقات المرتبطة (Associated Domains) في آبل وجوجل.
|
||||
</div>
|
||||
|
||||
### المرحلة الثانية: تحديثات وتساب (Baileys Node.js)
|
||||
<div dir="rtl" align="right">
|
||||
١. إضافة واجهة التحقق من وجود الرقم على خادم وتساب.
|
||||
٢. تفعيل مكتبة توليد الصور برمجياً في خادم PHP لإرسال الصور التلقائية عبر وتساب.
|
||||
</div>
|
||||
|
||||
### المرحلة الثالثة: أتمتة تطبيق الهاتف والبدائل
|
||||
<div dir="rtl" align="right">
|
||||
١. دمج قارئ الإشعارات في فلاتر وتجربة الأتمتة الكاملة.
|
||||
٢. ربط قنوات الاتصال الفائت والرسائل النصية كقنوات بديلة نهائية في حال عدم نجاح التحقق عبر وتساب أو تيليجرام.
|
||||
</div>
|
||||
45
receiver_app_new/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
45
receiver_app_new/.metadata
Normal file
@@ -0,0 +1,45 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: android
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: ios
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: linux
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: macos
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: web
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: windows
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
17
receiver_app_new/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# receiver_app_new
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
28
receiver_app_new/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
14
receiver_app_new/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
49
receiver_app_new/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,49 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.intaleq.receiver"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.intaleq.receiver"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
45
receiver_app_new/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="receiver_app_new"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.intaleq.receiver
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
receiver_app_new/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
receiver_app_new/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
2
receiver_app_new/android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
5
receiver_app_new/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
26
receiver_app_new/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
34
receiver_app_new/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
24
receiver_app_new/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
2
receiver_app_new/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
2
receiver_app_new/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
43
receiver_app_new/ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
29
receiver_app_new/ios/Podfile.lock
Normal file
@@ -0,0 +1,29 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
753
receiver_app_new/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,753 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
C1F316133B487874ED8A74A6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB2FAD8495A0632447656C56 /* Pods_Runner.framework */; };
|
||||
DB5E52690D37376F6953FAAE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
1D3FDD200DC57EFCC3345D5E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
54380F195E4C3CF1735EDB9B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A18908AE936C3BF25319DFEF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
CB2FAD8495A0632447656C56 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
3DEC94A83268B3E53CD4EC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB5E52690D37376F6953FAAE /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C1F316133B487874ED8A74A6 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6E02E11B64335E94F3ADE1BF /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54380F195E4C3CF1735EDB9B /* Pods-Runner.debug.xcconfig */,
|
||||
1D3FDD200DC57EFCC3345D5E /* Pods-Runner.release.xcconfig */,
|
||||
A18908AE936C3BF25319DFEF /* Pods-Runner.profile.xcconfig */,
|
||||
58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */,
|
||||
1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
6E02E11B64335E94F3ADE1BF /* Pods */,
|
||||
E19875AD42182341B17CC104 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E19875AD42182341B17CC104 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CB2FAD8495A0632447656C56 /* Pods_Runner.framework */,
|
||||
91B2FA75278AC3239EEC00CD /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
E355C13815ED1617EEB83E5C /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
3DEC94A83268B3E53CD4EC5E /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
09F30E8C67E295B11062BC79 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
044E088535EBDE98F25D3197 /* [CP] Embed Pods Frameworks */,
|
||||
03DEDA86686B8509D18B6C8A /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
03DEDA86686B8509D18B6C8A /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
044E088535EBDE98F25D3197 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
09F30E8C67E295B11062BC79 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
E355C13815ED1617EEB83E5C /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 58139DEC46478EC4C58ABD53 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6D4F9875F678077728BADDC5 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1A54C71E55FFF4462C2CCEFF /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.intaleq.receiverAppNew;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
7
receiver_app_new/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
receiver_app_new/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
16
receiver_app_new/ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |