Gxo Order Tracking
1. Overview
This document explains how GXO Order Tracking is implemented in the Toolstation mobile app.
Toolstation supports two types of order tracking:
- Standard Tracking
- FastTrack
- Legacy carrier URLs
- Redirect to external browser
- GXO Tracking (In-App)
- API-based
- Real-time tracking events
- Displayed directly inside the app UI
GXO is the preferred tracking method, but only for orders that meet specific eligibility criteria.
2. Standard vs GXO – Decision Flow
This is the actual logic used in OrderHistoryDetailPageBloc:
flowchart TD
%% ================================
%% USER → ORDER FLOW
%% ================================
A["User opens OrderHistoryPage"]
B["User taps an order"]
C["OrderHistoryDetailPage loads\n(order + tracking links)"]
D["User taps 'Track Order'"]
A --> B --> C --> D
%% ================================
%% BLOC DECISION FLOW
%% ================================
E["BLoC: Is GXO Eligible?"]
D --> E
E -->|Yes| F["Extract trackingId"]
F --> G["Navigate to OrderTrackingPage"]
G --> H["BLoC fetches GXO tracking data"]
H --> I["UI shows: loading / timeline / error"]
%% ================================
%% STANDARD TRACKING (Fallback)
%% ================================
E -->|No| S["Use Standard Tracking"]
S --> T{"Tracking Type?"}
T -->|FastTrack| U["FastTrackTrackingPage"]
T -->|"External URL"| V["Open WebView"]
T -->|"No link"| W["Show 'Tracking Unavailable'"]
3. GXO Eligibility Criteria
- GXO Order Tracking eligibility is evaluated in two distinct stages:
Stage 1 — Determine Which Tracking Flow to Use
- This stage decides whether the app should attempt to use GXO tracking at all.
- Feature Flag Check (Mandatory)
- We only consider GXO tracking if the feature flag is true:
bool get hasGXOTracking => isFeatureFlagEnabled(FeatureFlag.gxoTracking);- Mandatory:
true - If false, the app will fall back to the legacy tracking flow immediately.
- No further GXO checks are performed.
Stage 2 — Determine Whether to Enable the GXO Track Button
- Once GXO tracking is selected as the tracking flow (Stage 1), the app performs additional eligibility checks to decide if the "Track Order" button should be enabled for this specific order.
- GXO order-level eligibility is implemented in:
lib/bloc/order/order_tracking_bloc.dart - An order is considered GXO-eligible only when ALL of the following are true:
- Order belongs to Default Branch (Delivery Order)
- Mandatory:
true - We are checking here for the delivery Order only because Order Tracking is only available on those order.
order.branchId == Strings.defaultBranchId - Mandatory:
- Order Status is Completed
order.status == OrderStatus.completed - Order Date ≥ GXO Cutoff Date
(order.completedDate ?? order.orderDate).isAfter(orderRepository.gxoCutoffDate)
4. GXO Configuration
File: lib/model/configs/gxo_config.dart
Used by _GXOTrackingService to determine:
- API base URL
- Cutoff date for eligibility
const String _gxoBaseUrl = 'https://api-gxo.gke.pre-prod.nl.toolstation.dev';
@JsonSerializable()
class GXOConfig {
const GXOConfig({this.baseUrl = _gxoBaseUrl, this.cutoffDate});
final String baseUrl;
@JsonKey(fromJson: _cutoffDateFromJson)
final DateTime? cutoffDate;
static DateTime? _cutoffDateFromJson(dynamic value) {
try {
return DateTime.parse(value);
} catch (_) {
return null;
}
}
}
Notes:
baseUrlcomes from Remote Config (overridable).cutoffDatedefines whether the order supports GXO.
5. Complete User Navigation Flow
5.1 Order List → Order Details
- On the Order History Page (
order_history_page.dart) there is a sliverList which rendersOrderHistoryItemV2for each order.
OrderHistoryItemV2(
order: order,
onTap: () => Navigator.push(
context,
OrderHistoryDetailPage.routeOrder(order),
),
);
5.2 Order Details → Track Order Action
- When user click on
OrderHistoryItemV2then the app navigate to Order History Detail Page via the routerouteOrderand using that it creates theOrderHistoryDetailPageBlocwhich callsloadDatamethod that fetch order details from the repository. - The
loadDataalso calls_loadTrackingDataand updates the response in the state, which contains tracking links for that specific order.
The page listens for bloc events:
_eventSub = bloc.eventStream.listen(_onBlocEvent);
- After that, when user clicks on the Track Order button this method get called
VoidCallback? handleTrackOrderButtonPressed(Order order, OrderTrackerData? orderTrackerData) {
final fastTrackTrackingUrl = order.fastTrack?.trackingMapUrl;
if (fastTrackTrackingUrl.isNotNullAndNotEmpty) {
return () => trackOrder(fastTrackTrackingUrl!);
} else if (hasGXOTracking && _isEligibleForGxo(order)) {
return () => _addGxoNavigationEvent(orderTrackerData);
} else if (orderTrackerData != null) {
return _fetchAndTrackUrl;
}
return null;
}
void _addGxoNavigationEvent(OrderTrackerData? orderTrackerData) {
if (orderTrackerData?.hasLinks != true) return;
final url = orderTrackerData!.urls.first;
final trackingId = _extractTrackingIdFromUrl(url);
_event.add(TrackOrderNavigationEvent(trackingId));
}
String? _extractTrackingIdFromUrl(String url) {
final uri = Uri.tryParse(url);
if (uri != null && uri.queryParameters.containsKey('id')) {
return uri.queryParameters['id'];
}
return null;
}
- The
handleTrackOrderButtonPressedmethod checks if the order is eligible for GXO and if so, it emits theTrackOrderNavigationEventevent.
When GXO applies, the bloc emits:
emit(TrackOrderNavigationEvent(trackingId: trackingId));
- The
bloc.eventStream.listen(_onBlocEvent)listen to event and navigate user toOrderTrackingPage.
Navigator.push(
context,
OrderTrackingPage.route(trackingId: event.trackingId),
);
- When the user navigates to
OrderTrackingPagethen it callsbloc.fetchTrackingData(widget.trackingId!)and sends the tracking ID to the bloc to fetch tracking data.
Future<void> fetchTrackingData(String trackingId) async {
trackingId = trackingId.trim();
if (trackingId.isEmpty) return;
_state.add(
currentState.copyWith(
isLoading: true,
errorCode: null,
isGenericError: false,
trackingData: null,
),
);
try {
final response = await _orderRepository.getGxoTrackingData(trackingId);
_state.add(
currentState.copyWith(
isLoading: false,
trackingData: response.trackingData,
errorCode: null,
),
);
} on HttpServiceException catch (e, st) {
LoggingService.log('fetch-gxo-tracking-data-failed', e, st);
_state.add(
currentState.copyWith(
isLoading: false,
trackingData: null,
errorCode: e.statusCode,
),
);
} catch (e, st) {
LoggingService.log('fetch-gxo-tracking-data-failed', e, st);
_state.add(
currentState.copyWith(
isLoading: false,
trackingData: null,
errorCode: null,
isGenericError: true,
),
);
}
}
- Then, on the
OrderTrackingPageUI is wrapped under BlocConsumer to listen to the bloc state and update the UI accordingly.
BlocConsumer<OrderTrackingPageBloc, OrderTrackingPageBlocState>(
builder: (context, bloc, state, child) {
return FullPageSpinner(
isLoading: state.isLoading,
child: Column(
children: [
verticalMargin16,
OrderTrackingSearchBar(
duration: const Duration(milliseconds: 200),
elevation: 2.0,
controller: _searchController,
focusNode: _focusNode,
active: _focusNode.hasFocus,
loading: state.isLoading,
onClearPressed: _onClear,
onSubmitted: bloc.fetchTrackingData,
),
Expanded(
child: Builder(
builder: (context) {
if (state.errorCode != null || state.isGenericError) {
return _OrderTrakingErrorBody(errorCode: state.errorCode);
} else if (state.trackingData != null) {
return OrderTrackingDetails(trackingData: state.trackingData!);
} else {
return const _OrderTrakingInitialBody();
}
},
),
),
],
),
);
},
),
6. Tracking ID Extraction Logic
Tracking links come from: GET /orders/{orderId}/tracking-links
Tracking Links Response:
{
"data": [
{
"carrier_name": "PostNL Pakket",
"tracking_links": [
"https://tracking.pre-prod.nl.toolstation.dev?id=3SMJZU418494330"
]
}
]
}
Example link:https://tracking.pre-prod.nl.toolstation.dev?id=3SMJZU418494330
Extraction of Tracking Id:
String? _extractTrackingIdFromUrl(String url) {
final uri = Uri.tryParse(url);
return uri?.queryParameters['id'];
}
7. OrderTrackingPage Lifecycle
order_tracking_page.dart
7.1 On Page Load
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
bloc.fetchTrackingData(widget.trackingId!);
});
}
8. OrderTrackingPageBloc (GXO Tracking BLoC)
order_tracking_page_bloc.dart
Responsibilities:
- Accept
trackingId - Start loading
- Call Repository
- Emit success or error states
State:
class OrderTrackingPageBlocState {
final bool isLoading;
final OrderTrackingData? trackingData;
final int? errorCode; // HTTP error
final bool isGenericError; // Unknown error
...
}
Fetch Flow:
Future<void> fetchTrackingData(String trackingId) async {
emit(state.copyWith(isLoading: true));
try {
final response = await _orderRepo.getGxoTrackingData(trackingId);
emit(state.copyWith(
isLoading: false,
trackingData: response.trackingData,
));
} on HttpServiceException catch (e) {
emit(state.copyWith(
isLoading: false,
errorCode: e.statusCode,
));
} catch (_) {
emit(state.copyWith(
isLoading: false,
isGenericError: true,
));
}
}
9. Repository Layer
order_repository.dart
Future<GXOOrderTrackingResponse> getGxoTrackingData(String trackingId) {
return _trackingService.fetchGxoTrackingData(trackingId);
}
10. Tracking Service Layer
order_tracking_service.dart
Abstract:
abstract class TrackingService {
DateTime get gxoCutoffDate;
Future<GXOOrderTrackingResponse> fetchGxoTrackingData(String trackingId);
}
GXO Implementation:
final response = await _dio.get(
'${gxoConfig.baseUrl}/ecom/v1/orders/tracking/$trackingId'
);
Errors:
if (response.statusCode != 200) {
throw HttpServiceException(
statusCode: response.statusCode!,
message: response.data,
);
}
11. GXO Response Model
model/order_tracking_data.dart
class GXOOrderTrackingResponse {
final OrderTrackingData trackingData;
GXOOrderTrackingResponse.fromJson(Map<String, dynamic> json)
: trackingData = OrderTrackingData.fromJson(json['tracking']);
}
OrderTrackingData Example:
class OrderTrackingData {
final String carrierName;
final DateTime? actualArrival;
final List<TrackingEvent> events;
}
TrackingEvent Example:
class TrackingEvent {
final String carrierStatus;
final String carrierDescription;
final DateTime dateUTC;
}
12. Complete API Example
Tracking Links Response:
{
"data": [
{
"carrier_name": "PostNL Pakket",
"tracking_links": [
"https://tracking.pre-prod.nl.toolstation.dev?id=3SMJZU418494330"
]
}
]
}
GXO Tracking Response:
{
"data": {
"tracking": {
"carrierName": "PostNL WS",
"actualArrival": "2025-08-14 09:24:00",
"events": [
{
"carrier_status": "O8",
"carrier_description": "Delivered",
"dateUTC": "2025-08-14T09:24:00"
}
]
}
}
}
13. Important Files Index
| Layer | File |
|---|---|
| UI | order_history_page.dartorder_history_detail_page.dartorder_tracking_page.dart |
| BLoC | order_history_detail_page_bloc.dartorder_tracking_page_bloc.dart |
| Repository | order_repository.dart |
| Service | order_tracking_service.dart |
| Models | order_tracking_data.dart |
| Config | gxo_config.dart |
GXO Order Tracking Feature Video: GXO Order Tracking Feature