Redesign on the Stitch reference, in Plus Jakarta Sans

Four passes, and the shape they landed on.

The type face is Plus Jakarta Sans (variable, wght 200-800), which brings a
fix with it: it carries the rupee glyph and Switzer does not, so prices stop
being set in Geist Mono to work around a missing character. Mono stays where
it is earned - references and phone numbers, read digit by digit.

Surfaces lift rather than outline. Cards carry two very soft shadow layers
instead of a hairline, because eight outlined boxes down a screen read as a
wireframe. The tab bar floats as a pill again for the same reason it was
right to: it is now the same kind of object as everything above it.

Screens:

* Home is the greeting, the address, the sphere and one card. The card lost
  its progress bar - a filling line says "wait", and a parcel two days into
  a journey is not something anyone is waiting through - and gained the size
  that buys.
* Orders cards are four bands: identity, destination, route, and whatever is
  happening right now. Plus a search field, because the list is the archive.
* Tracking leads with the state at display size, then TRIP MILESTONES with a
  step counter, then the courier.
* Review is a route thread over two particular cards.
* Account opens on the person: avatar, name, and two counted figures.

Three real bugs the redesign surfaced:

* Quick dispatch handed `loadCities()` straight to a FutureBuilder, so the
  catalogue was refetched on every rebuild and Home never settled.
* Order cards showed the whole visit's weight on one destination's row -
  somebody else's parcel. Per group now, and only once actually weighed.
* The pickup window was printed beside "In transit", where it reads as a
  delivery time nobody promised.

Nothing invented. The reference shows EXPRESS PRIORITY, CARBON OFFSET,
CONCIERGE ELITE and hub-to-hub routing; this backend sends none of them, so
they are absent rather than mocked up.

flutter analyze: clean. flutter test: 88 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
2026-09-24 11:08:25 +05:30
parent 06fa6b797a
commit 86b6af48c2
73 changed files with 3398 additions and 1722 deletions

View File

@@ -7,7 +7,6 @@ import 'package:doormile_cx/main.dart';
import 'package:doormile_cx/state/app_state.dart';
import 'package:doormile_cx/ui/widgets/buttons.dart';
import 'package:doormile_cx/ui/widgets/map_panel.dart';
import 'package:doormile_cx/ui/widgets/chrome.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -86,7 +85,7 @@ Future<void> serverAdvance(
/// and the form is what the answer leads to, so every booking test is
/// [openSend] followed by [pickCity].
Future<void> openSend(WidgetTester tester) async {
await tester.tap(find.text('BOOK'));
await tester.tap(find.text('BOOK NOW'));
await settle(tester);
}
@@ -99,6 +98,15 @@ Future<void> openSend(WidgetTester tester) async {
/// The destination is reached by searching rather than by opening its state:
/// search cuts across every state from the first step, and it is the path a
/// customer who already knows where they are sending actually takes.
/// A finder restricted to the open bottom sheet.
///
/// Home's live card heads with the district alone — "Chennai", not
/// "Chennai, Tamil Nadu" — so a bare `find.text('Chennai')` matches the screen
/// behind the scrim as well as the row in the sheet. Every assertion about
/// what a sheet is offering goes through this.
Finder inSheet(Finder matching) =>
find.descendant(of: find.byType(BottomSheet), matching: matching);
Future<void> pickCity(
WidgetTester tester,
String name, {
@@ -106,11 +114,20 @@ Future<void> pickCity(
}) async {
await tester.enterText(find.byType(TextField).first, name);
await settle(tester, 250);
// A `Text`, explicitly: `find.text` also matches `EditableText`, so once the
// city's name is in the search box the plain finder returns two widgets and
// `.first` is the box itself. Tapping that only refocuses it.
// A `Text` inside the sheet, explicitly.
//
// `find.text` also matches `EditableText`, so once the city's name is in the
// search box the plain finder returns the box as well — and Home, behind the
// scrim, now heads its live card with the district alone, so an unscoped
// finder can land on a widget the sheet is covering. Scoping to the modal
// route fixes both.
await tester.tap(
find.byWidgetPredicate((w) => w is Text && w.data == name).first,
find
.descendant(
of: find.byType(BottomSheet),
matching: find.byWidgetPredicate((w) => w is Text && w.data == name),
)
.first,
);
await settle(tester);
@@ -137,7 +154,7 @@ Future<void> pickCity(
Future<void> openPickupEditor(WidgetTester tester) async {
await openSend(tester);
await pickCity(tester, 'Chennai');
await tester.tap(find.text('FROM'));
await tester.tap(find.text('ORIGIN'));
await settle(tester);
}
@@ -192,17 +209,20 @@ void main() {
await settle(tester);
// All four digits arrived, so it verified and moved on.
expect(find.text('BOOK'), findsOneWidget);
expect(find.text('BOOK NOW'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('signs in and lands on Home', (tester) async {
await signIn(tester);
expect(find.text('Send a parcel'), findsOneWidget);
expect(find.text('BOOK'), findsOneWidget);
// The seeded active booking surfaces on Home.
expect(find.text('Miler assigned'), findsOneWidget);
// Home greets by name, carries the sphere, and ends on the live card.
expect(find.text('Hello, Joe'), findsOneWidget);
expect(find.text('BOOK NOW'), findsOneWidget);
// The seeded active booking surfaces on Home. Its state is the uppercase
// micro-label beside the destination, in the chip vocabulary — "Assigned",
// not the rail's "Miler assigned".
expect(find.text('ASSIGNED'), findsOneWidget);
await drainToasts(tester);
});
@@ -233,7 +253,7 @@ void main() {
await tester.tap(find.text('Use this address'));
await settle(tester);
// Back on the send screen: its title is the route, not a heading.
expect(find.text('TO'), findsOneWidget);
expect(find.text('DESTINATION'), findsOneWidget);
expect(find.text(corrected), findsOneWidget);
await drainToasts(tester);
});
@@ -282,7 +302,7 @@ void main() {
// …and that one point is what the booking carries forward.
await tester.tap(find.text('Use this address'));
await settle(tester);
expect(find.text('TO'), findsOneWidget);
expect(find.text('DESTINATION'), findsOneWidget);
expect(state.draftPickup!.lat, searched.lat);
expect(state.draftPickup!.lng, searched.lng);
expect(state.draftPickup!.title, 'Brookefields Mall');
@@ -398,8 +418,8 @@ void main() {
// Everything else the booking needs is on one screen, and the route to
// the top of it is two rows with no labels but FROM and TO.
expect(find.text('FROM'), findsOneWidget);
expect(find.text('TO'), findsOneWidget);
expect(find.text('ORIGIN'), findsOneWidget);
expect(find.text('DESTINATION'), findsOneWidget);
// Nothing is asked for here at all any more: no address fields, and no
// disclosure hiding three more. The route, the packages and the button.
expect(find.byType(TextField), findsNothing);
@@ -408,9 +428,9 @@ void main() {
// The customer answered the window sheet on the way in, so the booking
// already carries a slot they chose.
expect(state.draftSlotId, isNotNull);
expect(find.text('Book pickup'), findsOneWidget);
expect(find.text('Confirm Booking'), findsOneWidget);
await tester.tap(find.text('Book pickup'));
await tester.tap(find.text('Confirm Booking'));
await settle(tester);
expect(find.text('Pickup booked'), findsOneWidget);
@@ -441,7 +461,7 @@ void main() {
expect(state.draftContactPhone, isNull);
// Changed, it is the number the Miler will ring at the door.
await tester.tap(find.text('FROM'));
await tester.tap(find.text('ORIGIN'));
await settle(tester);
await tester.enterText(find.byType(TextField).first, '9003144518');
await settle(tester, 200);
@@ -458,12 +478,12 @@ void main() {
await openSend(tester);
await pickCity(tester, 'Coimbatore');
expect(find.text('1 package'), findsOneWidget);
expect(find.text('1 parcel'), findsOneWidget);
await tester.tap(find.byIcon(LucideIcons.plus));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.plus));
await tester.pump();
expect(find.text('3 packages'), findsOneWidget);
expect(find.text('3 parcels'), findsOneWidget);
expect(state.draftTotalPackages, 3);
// Push past the cap on the state; the clamp is the model's, not the UI's.
@@ -471,7 +491,7 @@ void main() {
await tester.pump();
expect(state.draftTotalPackages, state.limits.maxPackages);
await tester.tap(find.text('Book pickup'));
await tester.tap(find.text('Confirm Booking'));
await settle(tester);
expect(state.trackedBooking!.totalPackages, state.limits.maxPackages);
await drainToasts(tester);
@@ -485,9 +505,10 @@ void main() {
await pickCity(tester, 'Chennai');
final first = state.draftSlotId;
// The window lives in the footer now, beside the price it is quoted
// against, and the whole left column opens the sheet.
await tester.tap(find.text('PICKUP'));
// The window is a row on the review list, under the route. It carries no
// label — the clock glyph and the time are the whole row — so it is found
// by the window itself.
await tester.tap(find.textContaining('2:00 – 4:00 PM').first);
await settle(tester);
expect(find.text('Pickup window'), findsOneWidget);
@@ -508,26 +529,26 @@ void main() {
await openSend(tester);
// Step one is the states, not sixty district names.
expect(find.text('Tamil Nadu'), findsOneWidget);
expect(find.text('Kerala'), findsOneWidget);
expect(find.text('Chennai'), findsNothing);
expect(inSheet(find.text('Tamil Nadu')), findsOneWidget);
expect(inSheet(find.text('Kerala')), findsOneWidget);
expect(inSheet(find.text('Chennai')), findsNothing);
// Step two is that state's districts.
await tester.tap(find.text('Tamil Nadu'));
await settle(tester);
expect(find.text('Chennai'), findsOneWidget);
expect(find.text('Coimbatore'), findsWidgets);
expect(inSheet(find.text('Chennai')), findsOneWidget);
expect(inSheet(find.text('Coimbatore')), findsWidgets);
// Districts that are not open are never offered, and neither is a state
// whose every district is closed.
expect(find.text('Madurai'), findsNothing);
expect(find.text('Puducherry'), findsNothing);
expect(inSheet(find.text('Madurai')), findsNothing);
expect(inSheet(find.text('Puducherry')), findsNothing);
// Back out, then let search cut across states: Ernakulam is in Kerala and
// the customer should not have to know that to find it.
await tester.tap(find.byIcon(LucideIcons.arrowLeft).first);
await settle(tester);
expect(find.text('Chennai'), findsNothing);
expect(inSheet(find.text('Chennai')), findsNothing);
await pickCity(tester, 'Ernakulam');
expect(find.textContaining('Ernakulam'), findsWidgets);
@@ -538,19 +559,21 @@ void main() {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await settle(tester);
// Before collection: one row, owned by the pickup.
expect(find.text('DM-482913'), findsOneWidget);
// Before collection: one row, owned by the pickup. The reference shares a
// line with the date and the contents now, so it is a span inside a
// paragraph rather than a Text of its own.
expect(find.textContaining('DM-482913'), findsOneWidget);
// After collection: the seeded multi-destination pickup is in transit, so
// each destination stands on its own row.
final booking = state.byReference('DM-471200')!;
expect(booking.hasOrders, isTrue);
expect(find.text('Pickup DM-471200'), findsNothing);
expect(find.text('Ernakulam, Kerala'), findsOneWidget);
expect(find.text('Coimbatore, Tamil Nadu'), findsOneWidget);
expect(find.text('Ernakulam'), findsOneWidget);
expect(find.text('Coimbatore'), findsOneWidget);
// Each row is identified by its OWN tracking number, not by the visit that
// produced it. "Collected on DM-471200" used to run under both rows; it
@@ -558,7 +581,7 @@ void main() {
// them, so it moved to the order's own screen.
expect(find.text('Collected on DM-471200'), findsNothing);
for (final group in booking.destinations) {
expect(find.text(group.trackingId!), findsOneWidget);
expect(find.textContaining(group.trackingId!), findsOneWidget);
}
// Independently tracked.
expect(
@@ -566,10 +589,12 @@ void main() {
booking.destinations.length,
);
// Opening one follows that order, not the whole visit.
await tester.tap(find.text('Ernakulam, Kerala'));
// Opening one follows that order, not the whole visit: the head card's
// route ends at that destination, and the reference is its own tracking id.
await tester.tap(find.text('Ernakulam'));
await settle(tester);
expect(find.text('To Ernakulam, Kerala'), findsOneWidget);
expect(find.text('Ernakulam, Kerala'), findsWidgets);
expect(find.text('YOUR PACKAGES'), findsOneWidget);
expect(find.text('YOUR PACKAGES'), findsOneWidget);
await drainToasts(tester);
});
@@ -579,7 +604,10 @@ void main() {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
// The live card, by its reference. Home's quick-dispatch chips are
// named after cities too, so a bare 'Chennai' finder now hits a
// shortcut that starts a new booking instead.
await tester.tap(find.text('DM-482913'));
await settle(tester);
// The whole shipment is on screen the moment tracking opens — five rows
@@ -613,12 +641,17 @@ void main() {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
// The live card, by its reference. Home's quick-dispatch chips are
// named after cities too, so a bare 'Chennai' finder now hits a
// shortcut that starts a new booking instead.
await tester.tap(find.text('DM-482913'));
await settle(tester);
// The live card, not an eyebrow: a pickup in flight is the screen's one
// crimson surface.
expect(find.text('LIVE'), findsOneWidget);
// The head card names the state in a pill and draws the route as a rail.
// It used to be a filled crimson card headed "LIVE" — crimson is the
// action colour, and tracking is a screen you read.
expect(find.text('Miler assigned'), findsWidgets);
expect(find.text('12 Nehru Street'), findsWidgets);
expect(find.text('Cancel this pickup'), findsOneWidget);
// No per-package breakdown before collection.
expect(find.text('YOUR PACKAGES'), findsNothing);
@@ -638,7 +671,10 @@ void main() {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
// The live card, by its reference. Home's quick-dispatch chips are
// named after cities too, so a bare 'Chennai' finder now hits a
// shortcut that starts a new booking instead.
await tester.tap(find.text('DM-482913'));
await settle(tester);
await tester.tap(find.text('Cancel this pickup'));
@@ -650,15 +686,19 @@ void main() {
await tester.tap(find.text('Yes, cancel pickup'));
await settle(tester);
expect(find.text('Booking cancelled'), findsOneWidget);
// The head card carries the state in its pill now, not as a headline.
expect(find.textContaining('Cancelled'), findsWidgets);
final booking = state.byReference('DM-482913')!;
expect(booking.status, BookingStatus.cancelled);
await drainToasts(tester);
});
testWidgets('shows the empty service-area state', (tester) async {
await signIn(tester);
// Set before signing in. Home's quick-dispatch row calls `loadCities()`
// as soon as it builds, which warms `statesCache` — good for the sheet,
// which now opens instantly, and fatal to a flag flipped afterwards.
api.flags.emptyServiceAreas = true;
await signIn(tester);
await openSend(tester);
@@ -688,12 +728,12 @@ void main() {
testWidgets('a delivered order opens its receipt', (tester) async {
await signIn(tester);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await settle(tester);
await tester.tap(find.text('Completed'));
await tester.tap(find.textContaining('Past'));
await settle(tester);
await tester.tap(find.text('Salem, Tamil Nadu'));
await tester.tap(find.text('Salem'));
await settle(tester);
expect(find.text('Delivered'), findsWidgets);
@@ -717,12 +757,12 @@ void main() {
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await signIn(tester); // login + OTP
expect(find.text('BOOK'), findsOneWidget); // home
expect(find.text('BOOK NOW'), findsOneWidget); // home
await openSend(tester); // the whole booking, one screen
await pickCity(tester, 'Chennai');
await tester.tap(find.text('Book pickup'));
await tester.tap(find.text('Confirm Booking'));
await settle(tester); // confirmation
await tester.tap(find.text('Track pickup'));
await settle(tester); // tracking
@@ -760,11 +800,15 @@ void main() {
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await signIn(tester);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await settle(tester);
await tester.tap(find.text('Completed'));
// At 1.8x the three tabs are wider than the screen, so the second one has
// to be scrolled to before it can be tapped.
await tester.ensureVisible(find.textContaining('Past'));
await tester.pump();
await tester.tap(find.textContaining('Past'));
await settle(tester);
await tester.tap(find.text('Salem, Tamil Nadu'));
await tester.tap(find.text('Salem'));
await settle(tester); // receipt
await drainToasts(tester);
@@ -774,22 +818,26 @@ void main() {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await settle(tester);
// Each tab carries its own count, inside the header's segmented control —
// and the figure is the rows that tab will actually show, which for a
// collected multi-destination pickup is one per destination.
// Each tab carries its own count — the figure is the rows that tab will
// actually show, which for a collected multi-destination pickup is one
// per destination. The count shares a paragraph with the label, so it is
// a span rather than a Text of its own.
expect(
find.descendant(
of: find.byType(DmSegmented),
matching: find.text('${state.entriesOf(BookingStatus.active).length}'),
find.textContaining(
'Active (${state.entriesOf(BookingStatus.active).length})',
),
findsOneWidget,
);
await tester.tap(find.text('Cancelled'));
// The tab row scrolls — three labels plus their counts do not fit a 390pt
// phone at large text sizes.
await tester.ensureVisible(find.textContaining('Cancelled').first);
await tester.pump();
await tester.tap(find.textContaining('Cancelled').first);
await settle(tester);
expect(find.text('Erode, Tamil Nadu'), findsOneWidget);
expect(find.text('Erode'), findsOneWidget);
await drainToasts(tester);
});
}

View File

@@ -28,12 +28,8 @@ import 'package:flutter_test/flutter_test.dart';
/// everything in Ahem's black boxes and the pictures are worthless.
Future<void> _loadFonts() async {
for (final (family, asset) in const [
// Every cut, because Poppins is static: a FontLoader given only one file
// renders w800 headings by smearing the one weight it has.
('Poppins', 'assets/fonts/Poppins-Medium.ttf'),
('Poppins', 'assets/fonts/Poppins-SemiBold.ttf'),
('Poppins', 'assets/fonts/Poppins-Bold.ttf'),
('Poppins', 'assets/fonts/Poppins-ExtraBold.ttf'),
('PlusJakartaSans', 'assets/fonts/PlusJakartaSans-Variable.ttf'),
('Switzer', 'assets/fonts/Switzer-Variable.ttf'),
('GeistMono', 'assets/fonts/GeistMono-VF.ttf'),
// The icon font too, or every glyph in the picture is a hollow box.
('packages/lucide_icons_flutter/Lucide',
@@ -152,25 +148,31 @@ void main() {
await tester.tapAt(const Offset(200, 40)); // through the scrim
await _settle(tester);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await _settle(tester);
await _shot(tester, '04-orders');
await tester.tap(find.text('Account'));
await tester.tap(find.bySemanticsLabel('Account'));
await _settle(tester);
await _shot(tester, '05-account');
await tester.tap(find.text('Home'));
await tester.tap(find.bySemanticsLabel('Home'));
await _settle(tester);
await tester.tap(find.text('Miler assigned'));
// The live card, by its reference. Home's quick-dispatch chips are
// named after cities too, so a bare 'Chennai' finder now hits a
// shortcut that starts a new booking instead.
await tester.tap(find.text('DM-482913'));
await _settle(tester);
await _shot(tester, '06-tracking');
await tester.dragUntilVisible(
find.text('Cancel this pickup'),
find.byType(Scrollable).first,
const Offset(0, -200),
);
// `ensureVisible`, not `dragUntilVisible`.
//
// `IndexedStack` keeps all three tab roots alive under the pushed route,
// so a positional `Scrollable` finder picks Home's list — which the
// tracking screen is covering. `ensureVisible` finds the target's own
// scrollable ancestor and needs no finder at all.
await tester.ensureVisible(find.text('Cancel this pickup'));
await tester.pump();
await tester.tap(find.text('Cancel this pickup'));
await _settle(tester);
await _shot(tester, '06b-cancel-sheet');
@@ -181,7 +183,7 @@ void main() {
await _settle(tester);
// Booking: state, district, window, then one screen.
await tester.tap(find.text('BOOK'));
await tester.tap(find.text('BOOK NOW'));
await _settle(tester);
await _shot(tester, '07-destination-states');
@@ -189,7 +191,10 @@ void main() {
await _settle(tester);
await _shot(tester, '07b-destination-districts');
await tester.tap(find.text('Chennai').first);
// The live card, by its reference. Home's quick-dispatch chips are
// named after cities too, so a bare 'Chennai' finder now hits a
// shortcut that starts a new booking instead.
await tester.tap(find.text('DM-482913'));
await _settle(tester);
// The dev backend allows several destinations, so the row ticked rather
// than closing the sheet — the choice is confirmed.
@@ -204,25 +209,25 @@ void main() {
// The map editor behind the FROM row: the one screen in the flow whose
// subject is a picture.
await tester.tap(find.text('FROM'));
await tester.tap(find.text('ORIGIN'));
await _settle(tester);
await _shot(tester, '10-pickup-map');
await tester.tap(find.byIcon(LucideIcons.arrowLeft).first);
await _settle(tester);
await tester.tap(find.text('Book pickup'));
await tester.tap(find.text('Confirm Booking'));
await _settle(tester);
await _shot(tester, '11-booked');
// A finished order, for the receipt treatment.
await tester.tap(find.text('Back to home'));
await _settle(tester);
await tester.tap(find.text('Orders'));
await tester.tap(find.bySemanticsLabel('Orders'));
await _settle(tester);
await tester.tap(find.text('Completed'));
await tester.tap(find.textContaining('Past'));
await _settle(tester);
await _shot(tester, '04b-orders-completed');
await tester.tap(find.text('Salem, Tamil Nadu'));
await tester.tap(find.text('Salem'));
await _settle(tester);
await _shot(tester, '12-receipt');

View File

@@ -13,7 +13,7 @@ import 'package:http/testing.dart';
/// against the customer API document.
///
/// These are not parser tests. Every one of them exists because a plausible,
/// readable, wrong spelling — `otp` for `code`, `lat` for `latitude`, `min` for
/// readable, wrong spelling — `otp` for `code`, `latitude` for `lat`, `min` for
/// `minRupees` — costs a whole feature at runtime and nothing at compile time.
///
/// Where the written contract and the running server disagree, these tests
@@ -96,9 +96,18 @@ void main() {
],
);
// ── `lat`/`lng`, not `latitude`/`longitude` ──
//
// The written contract says the long spelling and this app sent it. The
// server reads the short one, so it saw a pickup with no coordinates, could
// not place it in a serviceable area, and answered every booking with
// 422 `unserviceable`. Verified against production on 2026-09-23 one
// request apart — same pickup, same slot: long spelling 422, short
// spelling 201 and booking DM-252803.
final pickup = t.sent.body['pickup'] as Map<String, dynamic>;
expect(pickup['latitude'], 13.0827);
expect(pickup['longitude'], 80.2707);
expect(pickup['lat'], 13.0827);
expect(pickup['lng'], 80.2707);
expect(pickup.containsKey('latitude'), isFalse);
final destination =
(t.sent.body['destinations'] as List).single as Map<String, dynamic>;
@@ -176,7 +185,7 @@ void main() {
final body = t.sent.body;
final pickup = body['pickup'] as Map<String, dynamic>;
expect(pickup['latitude'], 13.0827);
expect(pickup['lat'], 13.0827);
expect(pickup['contactName'], 'Alex Kumar');
expect(pickup['contactPhone'], '+919876543210');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 183 KiB