In the world of e-commerce returns, "Mail Returns" (where a customer ships a parcel themselves) are often a black hole. Because the system doesn't generate a label, there is no automatic tracking. No one knows where the package is, and the "he-said-she-said" over shipping proof becomes a support nightmare.
We recently tackled this by building a manual tracking flow that serves two distinct masters: Retailers needing oversight and Customers needing peace of mind. Here is how we built a unified tracking system across two different UIs with a shared backend.
The Problem: The "Mail Return" Black Hole
When a return is marked as Mail, the customer uses their own shipping method (e.g., national post or a local courier).
For Staff: They can't see when a return is coming or if it's even been sent.
For Customers: They have no way to "prove" they shipped it within the platform.
The Technical Gap: Since no label is generated by our system, no tracking data is auto-filled.
We needed a way for both parties to manually input Tracking Numbers and Courier Names into a shared source of truth.
The Architecture: One Logic, Two Gates
Instead of building two separate tracking services, we opted for a shared form object pattern. This ensures that whether a retailer updates a number or a customer does it, the validation and persistence logic remain identical.
1. The Backend Foundation
We leveraged a Shipment::UpdateForm object to handle the heavy lifting. This kept our controllers thin and our models clean.
Feature |
Implementation Detail |
|
Endpoints |
|
|
Validation |
Max 64 chars for tracking; Max 128 for courier; Alphanumeric only. |
|
State Guard |
Only allows updates if the return is in |
|
Data Model |
Updates |
Pro-Tip: Avoid heavy model callbacks. By performing the update within the Form Object’s
submitmethod, we ensured that side effects (like invalidating cache) only happened when the form was actually valid.
The Frontend Implementation
Retailer Experience (React + Polaris)
In the retailer dashboard, we focused on speed and clarity for support agents.
Contextual UI: If the shipping method is "Mail," an "Add Tracking Number" button appears.
The Modal Flow: Clicking the button triggers a Polaris modal that pre-fills existing data if the agent is editing rather than adding.
Immediate Feedback: Upon saving, the app invalidates the return order query, triggering an instant UI refresh to show the new "Track" button (which links to the external tracking URL).
Customer Experience (Remix + React)
The customer-facing side needed to be more "walk-through" style. We integrated this directly into the Return Summary page.
Progressive Disclosure: We show the "Tracking Number" and "Courier" labels even if empty to signal that this information is expected.
Inline Editing: Instead of a modal, we used an inline form. The "Save" button remains disabled until both fields are filled to ensure data integrity.
I18n Ready: Since we operate globally, all labels (
add_tracking_button_label, etc.) are served via the API to support multi-language storefronts.
Key Technical Takeaways
Shared Validation is King: By using a single Form Object, we prevented "data drift" where a retailer might be able to enter a character that a customer’s UI would reject.
Status-Awareness: Don't let users edit tracking after a return is "Completed" or "Cancelled." Hard-coding these guards into the service layer prevents API abuse.
The "Track" Button Logic: We implemented a fallback mechanism. The UI displays the
custom_courier_nameif present, but falls back to the system'scourierNamefor legacy data, ensuring the "Track Shipment" link never breaks.
Final Thoughts
Manual tracking isn't just about adding a text field; it's about creating a reliable audit trail. By building a unified backend and tailoring the UI to the specific needs of retailers and customers, we turned a "black hole" into a transparent part of the return lifecycle.
Top comments (0)