production
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,12 @@ import 'package:miler/data/api_config.dart';
|
||||
/// ── Why this file exists ──
|
||||
///
|
||||
/// The delivery screens under `lib/xpress` are a verbatim port of the
|
||||
/// Xpress-rider app, which talked to a different backend entirely:
|
||||
/// `jupiter.nearle.app` for reads and `queue.workolik.com` for writes, with the
|
||||
/// URLs scattered inline across `homepage.dart`, `cartpage.dart`,
|
||||
/// `deliveries.dart`, `summary.dart` and three controllers. Repointing that at
|
||||
/// Doormile by find/replacing hostnames is exactly the class of change that
|
||||
/// produced the `deliver`→`Pick up` corruption this repo has already had to
|
||||
/// undo once.
|
||||
/// Xpress-rider app, which talked to a **retired backend** entirely — two
|
||||
/// hosts, one for reads and one for writes, with the URLs scattered inline
|
||||
/// across `homepage.dart`, `cartpage.dart`, `deliveries.dart`, `summary.dart`
|
||||
/// and three controllers. Repointing that at Doormile by find/replacing
|
||||
/// hostnames is exactly the class of change that produced the
|
||||
/// `deliver`→`Pick up` corruption this repo has already had to undo once.
|
||||
///
|
||||
/// So every delivery-line URL is named here, once, and the call sites read a
|
||||
/// getter. Repointing the line is now one file.
|
||||
@@ -46,7 +45,7 @@ import 'package:miler/data/api_config.dart';
|
||||
/// be thin.
|
||||
///
|
||||
/// This is a known, accepted gap: it was chosen deliberately over keeping the
|
||||
/// working Nearle hosts. See the note in `docs/` if one is added.
|
||||
/// retired platform's hosts alive. See the note in `docs/` if one is added.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
class DeliveryApi {
|
||||
DeliveryApi._();
|
||||
@@ -173,9 +172,9 @@ class DeliveryApi {
|
||||
|
||||
/// **ABSENT** — image upload for a support ticket attachment.
|
||||
///
|
||||
/// Was `/api/v1/partners/uploadimage/`, with the result served back from
|
||||
/// `images.nearle.app`. Doormile has no upload route and no image host, so
|
||||
/// attachments cannot be sent.
|
||||
/// Was `/api/v1/partners/uploadimage/` on the retired platform, with the
|
||||
/// result served back from its own image host. Doormile has no upload route
|
||||
/// and no image host, so attachments cannot be sent.
|
||||
static String get uploadImage => url('/miler/support/upload');
|
||||
|
||||
/// Public base for an uploaded image. **ABSENT** — see [uploadImage].
|
||||
|
||||
@@ -1,260 +1,260 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Riderlog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
|
||||
///
|
||||
/// **Empty since the delivery line moved to Doormile.** The one entry here
|
||||
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
|
||||
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
|
||||
/// host any more — every delivery-line write goes to `api.doormile.com` via
|
||||
/// [DeliveryApi] — so the pin was routing advice about a server this app no
|
||||
/// longer talks to.
|
||||
///
|
||||
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
|
||||
/// the old backend, and pinning it for `api.doormile.com` would send every
|
||||
/// request to the wrong server with the right SNI. If Doormile ever needs the
|
||||
/// same workaround, add it here with its own verified address.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
// Use known-good IP if available, else resolve normally (prefer IPv4)
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host, // SNI = original hostname for Nginx routing
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
const maxAttempts = 3;
|
||||
try {
|
||||
debugPrint('createRiderLog payload ${json.encode(data)}');
|
||||
} catch (_) {}
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createRiderLog url $urldata (attempt $attempt)');
|
||||
debugPrint('createRiderLog status ${response.statusCode}');
|
||||
debugPrint('createRiderLog response ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint(
|
||||
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
|
||||
);
|
||||
// On 404/5xx, wait and retry to potentially hit a different CDN node
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('createRiderLog exception (attempt $attempt): $e');
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('createRiderLog failed after $maxAttempts attempts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> updateRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('updateRiderLog url: $urldata');
|
||||
debugPrint('updateRiderLog response: ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Exception in updateRiderLog: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
|
||||
Map<String, dynamic>? getRiderLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderLog response ${response.body}');
|
||||
debugPrint('getRiderLog url ${urldata.toString()}');
|
||||
getRiderLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
|
||||
Map<String, dynamic>? getRiderCountResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderCount response ${response.body}');
|
||||
debugPrint('getRiderCount url ${urldata.toString()}');
|
||||
getRiderCountResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderCountResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createBreakRiderLog url $urldata');
|
||||
debugPrint('createBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('updateBreakRiderLog url $urldata');
|
||||
debugPrint('updateBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
}
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Riderlog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
|
||||
///
|
||||
/// **Empty since the delivery line moved to Doormile.** The one entry here
|
||||
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
|
||||
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
|
||||
/// host any more — every delivery-line write goes to `api.doormile.com` via
|
||||
/// [DeliveryApi] — so the pin was routing advice about a server this app no
|
||||
/// longer talks to.
|
||||
///
|
||||
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
|
||||
/// the old backend, and pinning it for `api.doormile.com` would send every
|
||||
/// request to the wrong server with the right SNI. If Doormile ever needs the
|
||||
/// same workaround, add it here with its own verified address.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
// Use known-good IP if available, else resolve normally (prefer IPv4)
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host, // SNI = original hostname for Nginx routing
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
const maxAttempts = 3;
|
||||
try {
|
||||
debugPrint('createRiderLog payload ${json.encode(data)}');
|
||||
} catch (_) {}
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createRiderLog url $urldata (attempt $attempt)');
|
||||
debugPrint('createRiderLog status ${response.statusCode}');
|
||||
debugPrint('createRiderLog response ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint(
|
||||
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
|
||||
);
|
||||
// On 404/5xx, wait and retry to potentially hit a different CDN node
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('createRiderLog exception (attempt $attempt): $e');
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('createRiderLog failed after $maxAttempts attempts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> updateRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('updateRiderLog url: $urldata');
|
||||
debugPrint('updateRiderLog response: ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Exception in updateRiderLog: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
|
||||
Map<String, dynamic>? getRiderLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderLog response ${response.body}');
|
||||
debugPrint('getRiderLog url ${urldata.toString()}');
|
||||
getRiderLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
|
||||
Map<String, dynamic>? getRiderCountResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderCount response ${response.body}');
|
||||
debugPrint('getRiderCount url ${urldata.toString()}');
|
||||
getRiderCountResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderCountResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createBreakRiderLog url $urldata');
|
||||
debugPrint('createBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('updateBreakRiderLog url $urldata');
|
||||
debugPrint('updateBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class _BottomPageState extends State<BottomPage> {
|
||||
body: IndexedStack(index: selected, children: _pages),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Color(0xFFE9EDF2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
@@ -57,7 +57,7 @@ class _BottomPageState extends State<BottomPage> {
|
||||
],
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFFE9EDF2),
|
||||
currentIndex: selected,
|
||||
onTap: (index) => setState(() => selected = index),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
|
||||
Reference in New Issue
Block a user