Skip to content

6. HTTP clients for the tax calculation service

Image

6.1. Introduction

This chapter reproduces, without any structural changes, the three successive layers of clients built in the TypeScript course, each refining the previous one: a first client, CLI, where all calculations remain on the server side; a second client, CLI, where the calculation moves to the client side ([business] layer); a third client, compiled for the BROWSER; and finally, a complete client, HTML, with its own views. All four rely on the same NestJS server, which was installed in the previous chapter.

6.2. HTTP client 1

Image

6.2.1. The [dao] layer

6.2.2. [dao1] script

// dao1.dart
// Ports Dao1.ts: the [dao] layer (Data Access Object) encapsulates all the
// details of HTTP communication with the tax calculation server (URL,
// method, session cookie) behind simple business methods
// (initSession, authenticateUser, calculateTax...).
import 'package:dio/dio.dart';

class Dao1 {
  final Dio _dio;
  final String _sessionCookieName;
  String _sessionCookie = '';

  // constructor - receives the Dio instance to use, as the TypeScript
  // script received the axios instance as a parameter
  Dao1(this._dio) : _sessionCookieName = "connect.sid";

  // init session
  Future<dynamic> initSession() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'init-session', 'type': 'json'},
    );
  }

  Future<dynamic> authenticateUser(String user, String password) {
    return _getRemoteData(
      'main.php',
      method: 'POST',
      queryParameters: {'action': 'authenticate-user'},
      data: {'user': user, 'password': password},
      contentType: Headers.formUrlEncodedContentType,
    );
  }

  // tax calculation
  Future<dynamic> calculateTax(String married, int children, int salary) {
    return _getRemoteData(
      'main.php',
      method: 'POST',
      queryParameters: {'action': 'calculate-tax'},
      data: {'married': married, 'children': children, 'salary': salary},
      contentType: Headers.formUrlEncodedContentType,
    );
  }

  // list of simulations
  Future<dynamic> listSimulations() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'list-simulations'},
    );
  }

  // delete a simulation
  Future<dynamic> deleteSimulation(int index) {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'delete-simulation', 'number': index},
    );
  }

  Future<dynamic> _getRemoteData(
    String path, {
    required String method,
    Map<String, dynamic>? queryParameters,
    Object? data,
    String? contentType,
  }) async {
    // for the session cookie
    final options = Options(
      method: method,
      contentType: contentType,
      headers: {'Cookie': _sessionCookie},
      validateStatus: (_) => true,
    );
    // executing the HTTP request
    final response = await _dio.request(
      path,
      queryParameters: queryParameters,
      data: data,
      options: options,
    );
    // we retrieve the session cookie if it exists
    final setCookie = response.headers['set-cookie'];
    if (setCookie != null) {
      for (final header in setCookie) {
        final result = RegExp('^($_sessionCookieName.+?);').firstMatch(header);
        if (result != null) {
          _sessionCookie = result.group(1)!;
          break;
        }
      }
    }
    // the server's response is in [response.data]
    return response.data;
  }
}

Let's comment on this code:

  • Line 48: The [dao] layer (Data Access Object) encapsulates all HTTP communication details (URL, method, session cookie) behind simple business methods - [initSession], [authenticateUser], [calculateTax], [listSimulations], [deleteSimulation] (line 57).
  • Line 15: [Dao1(this._dio)] receives the [Dio] instance to be used as a constructor parameter, exactly as the TypeScript script received the [axios] instance.
  • Line 65: [_getRemoteData] (private, prefix [_]) centralizes manual tracking of the session cookie ([response.headers['set-cookie']], [RegExp] (line 90)) already encountered in dart_dio_03.dart: each public method of DAO relies on this same private method.

6.2.3. The [main1]

6.2.4. script [main1]

// main1.dart
// Ports main1.ts.
import 'dart:convert';
import 'package:dio/dio.dart';
import 'dao1.dart';

Future<void> main() async {
  // Dio configuration - global baseUrl/timeout, like axios.defaults in
  // TypeScript
  final dio = Dio(BaseOptions(
    baseUrl: 'http://localhost:3000/',
    connectTimeout: const Duration(seconds: 2),
    receiveTimeout: const Duration(seconds: 2),
  ));
  // instantiating the [dao] layer
  final dao = Dao1(dio);
  // using the [dao] layer
  try {
    // init session
    log("-----------init-session");
    dynamic response = await dao.initSession();
    log(response);
    // authentication
    log("-----------authenticate-user");
    response = await dao.authenticateUser("admin", "admin");
    log(response);
    // tax calculations - Future.wait is the Dart equivalent of Promise.all
    // (already seen in async_05.dart)
    log("-----------calculate-tax x 3");
    response = await Future.wait([
      dao.calculateTax("yes", 2, 45000),
      dao.calculateTax("no", 2, 45000),
      dao.calculateTax("no", 1, 30000),
    ]);
    log(response);
    // list of simulations
    log("-----------list-of-simulations");
    response = await dao.listSimulations();
    log(response);
    // deleting a simulation
    log("-----------deleting simulation no. 1");
    response = await dao.deleteSimulation(1);
    log(response);
  } catch (error) {
    // we log the error
    print("error=$error");
  }
}

// JSON log
void log(Object? object) {
  const JsonEncoder encoder = JsonEncoder.withIndent('  ');
  print(encoder.convert(object));
}

Let’s comment on this code:

  • line 16: [final dao = Dao1(dio);]: the [dao] layer is instantiated only once, then reused for the entire sequence of calls (init-session, authentication, calculations, list, deletion).
  • Lines 30–31: [Future.wait([dao.calculateTax(...), ...])]: the three tax calculations are launched IN PARALLEL, the Dart equivalent of [Promise.all], which was already encountered in async_05.dart.
  • Lines 52–53: [JsonEncoder.withIndent(' ').convert(object)]: The utility function [log] replicates the indented display of [JSON.stringify(x, null, 2)].

The result of the execution is as follows:

"-----------init-session"
{
"action": "init-session",
"status": 700,
"response": "session started with type [json]"
}
"-----------authenticate-user"
{
"action": "authenticate-user",
"status": 200,
"response": "Authentication successful [admin, admin]"
}
"-----------calculate-tax x 3"
[
{
"action": "calculate-tax",
"status": 300,
"response": {
"married": "yes",
"children": 2,
"salary": 45000,
"tax": 502,
"surcharge": 0,
"discount": 857,
"reduction": 126,
"rate": 0.14
}
},
{
"action": "calculate-tax",
"status": 300,
"response": {
"married": "no",
"children": 2,
"salary": 45000,
"tax": 3250,
"surcharge": 370,
"discount": 0,
"reduction": 0,
"rate": 0.3
}
},
{
"action": "calculate-tax",
"status": 300,
"response": {
"married": "no",
"children": 1,
"salary": 30000,
"tax": 1687,
"surcharge": 0,
"discount": 0,
"reduction": 0,
"rate": 0.14
}
}
]
"-----------list-of-simulations"
{
"action": "list-simulations",
"status": 500,
"response": [
{
"married": "yes",
"children": 2,
"salary": 45000,
"tax": 502,
"surcharge": 0,
"discount": 857,
"reduction": 126,
"rate": 0.14
},
{
"married": "no",
"children": 1,
"salary": 30000,
"tax": 1687,
"surcharge": 0,
"discount": 0,
"reduction": 0,
"rate": 0.14
}
]
}
"-----------deleting simulation no. 1"
{
"action": "delete-simulation",
"status": 600,
"response": [
{
"married": "yes",
"children": 2,
"salary": 45000,
"tax": 502,
"surcharge": 0,
"discount": 857,
"reduction": 126,
"rate": 0.14
}
]
}

6.3. HTTP client 2

Image

This second client moves the tax CALCULATION from the server to the client: the server now provides the raw tax data (rates, thresholds, etc.) only once via [get-admindata], and the [business] layer now performs the calculations.

6.3.1. The [Business]

6.3.2. script [business]

// business.dart
// Ports Business.ts: the tax calculation, done entirely on the client side
// (in the browser in TypeScript, here in the Dart program), from
// the raw tax data obtained only once from the server via
// [Dao2.getAdminData] (see dao2.dart).
class Business {
  // [dynamic]: the exact structure of the data sent by the tax
  // server (rates, ceilings...) is not documented here; as in
  // TypeScript, we deliberately stay flexible on this type
  final Map<String, dynamic> taxAdminData;

  Business(this.taxAdminData);

  // tax calculation
  // --------------------------------------------------------------------------
  // married: yes, no
  // children: number of children
  // salary: annual salary
  Map<String, dynamic> calculateTax(String married, int children, num salary) {
    // tax calculation with children
    final result1 = _calculateTax2(married, children, salary);
    final tax1 = result1['tax'] as num;
    // tax calculation without the children
    Map<String, dynamic> result2;
    num tax2;
    num halfShareCeiling;
    if (children != 0) {
      result2 = _calculateTax2(married, 0, salary);
      tax2 = result2['tax'] as num;
      // applying the family quotient cap
      halfShareCeiling = taxAdminData['halfShareQfCeiling'] as num;
      if (children < 3) {
        // halfShareCeiling for the first 2 children
        tax2 = tax2 - children * halfShareCeiling;
      } else {
        // halfShareCeiling for the first 2 children, double for the following ones
        tax2 = tax2 - 2 * halfShareCeiling - (children - 2) * 2 * halfShareCeiling;
      }
    } else {
      // no recalculation of the tax
      tax2 = tax1;
      result2 = result1;
    }
    // we take the higher tax in [tax1, tax2]
    num tax;
    num rate;
    num surcharge;
    if (tax1 > tax2) {
      tax = tax1;
      rate = result1['rate'] as num;
      surcharge = result1['surcharge'] as num;
    } else {
      surcharge = tax2 - tax1 + (result2['surcharge'] as num);
      tax = tax2;
      rate = result2['rate'] as num;
    }
    // calculation of a possible discount
    final discount = _getDiscount(married, tax);
    tax -= discount;
    // calculation of a possible tax reduction
    final reduction = _getReduction(married, salary, children, tax);
    tax -= reduction;
    // result
    return {
      'tax': tax.floor(),
      'surcharge': surcharge,
      'discount': discount,
      'reduction': reduction,
      'rate': rate,
    };
  }

  // --------------------------------------------------------------------------
  Map<String, dynamic> _calculateTax2(String married, int children, num salary) {
    // number of shares
    married = married.toLowerCase();
    num shares;
    if (married == "yes") {
      shares = children / 2 + 2;
    } else {
      shares = children / 2 + 1;
    }
    // 1 share per child from the 3rd one
    if (children >= 3) {
      // one more half-share for each child from the 3rd one
      shares += 0.5 * (children - 2);
    }
    // taxable income
    final taxableIncome = _getTaxableIncome(salary);
    // surcharge
    num surcharge = (taxableIncome - 0.9 * salary).floor();
    // for rounding issues
    if (surcharge < 0) {
      surcharge = 0;
    }
    // family quotient
    final quotient = taxableIncome / shares;
    // tax calculation
    final List<num> limits = List<num>.from(taxAdminData['limits'] as List);
    final List<num> coeffR = List<num>.from(taxAdminData['coeffR'] as List);
    final List<num> coeffN = List<num>.from(taxAdminData['coeffN'] as List);
    // is put at the end of the [limits] list to stop the following loop
    limits[limits.length - 1] = quotient;
    // looking for the tax rate
    var i = 0;
    while (quotient > limits[i]) {
      i++;
    }
    // because we placed quotient at the end of limits, the previous loop
    // cannot go beyond the limits list
    // now we can calculate the tax
    final tax = (taxableIncome * coeffR[i] - shares * coeffN[i]).floor();
    // result
    return {'tax': tax, 'surcharge': surcharge, 'rate': coeffR[i]};
  }

  // taxableIncome = annualSalary - allowance (the allowance has a min and a max)
  num _getTaxableIncome(num salary) {
    // allowance of 10% of the salary
    num allowance = 0.1 * salary;
    // this allowance cannot exceed tenPercentAllowanceMax
    final allowanceMax = taxAdminData['tenPercentAllowanceMax'] as num;
    if (allowance > allowanceMax) {
      allowance = allowanceMax;
    }
    // the allowance cannot be lower than tenPercentAllowanceMin
    final allowanceMin = taxAdminData['tenPercentAllowanceMin'] as num;
    if (allowance < allowanceMin) {
      allowance = allowanceMin;
    }
    // taxable income
    final taxableIncome = salary - allowance;
    return taxableIncome.floor();
  }

  // calculates a possible discount
  num _getDiscount(String married, num taxes) {
    // initially, a zero discount
    num discount = 0;
    // maximum tax amount to get the discount
    final taxCeilingForDiscount = married == "yes"
        ? taxAdminData['coupleTaxCeilingForDiscount'] as num
        : taxAdminData['singleTaxCeilingForDiscount'] as num;
    if (taxes < taxCeilingForDiscount) {
      // maximum amount of the discount
      final discountCeiling = married == "yes"
          ? taxAdminData['coupleDiscountCeiling'] as num
          : taxAdminData['singleDiscountCeiling'] as num;
      // theoretical discount
      discount = discountCeiling - 0.75 * taxes;
      // the discount cannot exceed the amount of the tax
      if (discount > taxes) {
        discount = taxes;
      }
      // no discount < 0
      if (discount < 0) {
        discount = 0;
      }
    }
    // result - ceil() is the Dart equivalent of Math.ceil()
    return discount.ceil();
  }

  // calculates a possible reduction
  num _getReduction(String married, num salary, int children, num taxes) {
    // the income ceiling to be entitled to the 20% reduction
    num incomeCeilingForReduction = married == "yes"
        ? taxAdminData['coupleIncomeCeilingForReduction'] as num
        : taxAdminData['singleIncomeCeilingForReduction'] as num;
    final halfShareReductionValue = taxAdminData['halfShareReductionValue'] as num;
    incomeCeilingForReduction += children * halfShareReductionValue;
    if (children > 2) {
      incomeCeilingForReduction += (children - 2) * halfShareReductionValue;
    }
    // taxable income
    final taxableIncome = _getTaxableIncome(salary);
    // reduction
    num reduction = 0;
    if (taxableIncome < incomeCeilingForReduction) {
      // 20% reduction
      reduction = 0.2 * taxes;
    }
    return reduction.ceil();
  }
}

Let’s break down this code:

  • line 12: [Business(this.taxAdminData)]: the constructor receives the raw tax data retrieved by DAO only once—no further network calls are required afterward to calculate a tax.
  • line 19: [Map<String, dynamic> calculateTax(String married, int children, num salary)]: a faithful, line-by-line port of the TypeScript algorithm—same steps ([_calculateTax2], [_getTaxableIncome], [_getDiscount], [_getReduction]), same logic for capping the family quotient.
  • [.floor()]/[.ceil()] (methods of [num]) replace [Math.floor(...)]/[Math.ceil(...)]: In Dart, these are instance methods on the number itself, not static methods of a utility class.
  • The KEYS of the returned [Map] ([tax], [surcharge], [discount], [reduction]...) have the same plain ASCII names as the Dart IDENTIFIERS (variable and parameter names)—a string may contain accents in Dart, but an identifier may not (see simulation.dart, later in this chapter).

6.3.3. The [Dao2]

6.3.4. script [dao2]

// dao2.dart
// Ports Dao2.ts: identical to dao1.dart for initSession and
// authenticateUser, but exposes [getAdminData] instead of
// [calculateTax]/[listSimulations]/[deleteSimulation] - the calculation and
// the list of simulations are now handled on the CLIENT side by the
// [business] layer (see business.dart), the server only serving to provide the
// raw tax data (rates, ceilings...).
import 'package:dio/dio.dart';

class Dao2 {
  final Dio _dio;
  final String _sessionCookieName;
  String _sessionCookie = '';

  Dao2(this._dio) : _sessionCookieName = "connect.sid";

  Future<dynamic> initSession() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'init-session', 'type': 'json'},
    );
  }

  Future<dynamic> authenticateUser(String user, String password) {
    return _getRemoteData(
      'main.php',
      method: 'POST',
      queryParameters: {'action': 'authenticate-user'},
      data: {'user': user, 'password': password},
      contentType: Headers.formUrlEncodedContentType,
    );
  }

  Future<dynamic> getAdminData() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'get-admindata'},
    );
  }

  Future<dynamic> _getRemoteData(
    String path, {
    required String method,
    Map<String, dynamic>? queryParameters,
    Object? data,
    String? contentType,
  }) async {
    final options = Options(
      method: method,
      contentType: contentType,
      headers: {'Cookie': _sessionCookie},
      validateStatus: (_) => true,
    );
    final response = await _dio.request(
      path,
      queryParameters: queryParameters,
      data: data,
      options: options,
    );
    final setCookie = response.headers['set-cookie'];
    if (setCookie != null) {
      for (final header in setCookie) {
        final result = RegExp('^($_sessionCookieName.+?);').firstMatch(header);
        if (result != null) {
          _sessionCookie = result.group(1)!;
          break;
        }
      }
    }
    return response.data;
  }
}

Let’s comment on this code:

  • line 4: Identical to [Dao1] for [initSession] and [authenticateUser], but exposes [getAdminData] instead of [calculateTax]/[listSimulations] (line 4)/[deleteSimulation] (line 4): These three operations are now handled on the CLIENT side by the [business] layer that was just introduced.

6.3.5. The [main2]

6.3.6. script [main2]

// main2.dart
// Ports main2.ts.
import 'dart:convert';
import 'package:dio/dio.dart';
import 'dao2.dart';
import 'business.dart';

Future<void> main() async {
  // Dio configuration
  final dio = Dio(BaseOptions(
    baseUrl: 'http://localhost:3000/',
    connectTimeout: const Duration(seconds: 2),
    receiveTimeout: const Duration(seconds: 2),
  ));
  // instantiating the [dao] layer
  final dao = Dao2(dio);
  // HTTP requests
  Map<String, dynamic> taxAdminData;
  try {
    // init session
    log("-----------init-session");
    dynamic response = await dao.initSession();
    log(response);
    // authentication
    log("-----------authenticate-user");
    response = await dao.authenticateUser("admin", "admin");
    log(response);
    // tax data
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response['response'] as Map<String, dynamic>;
  } catch (error) {
    // we log the error
    print("error=$error");
    // end
    return;
  }

  // instantiating the [business] layer
  final business = Business(taxAdminData);

  // tax calculations
  log("-----------calculate-tax x 3");
  final List<Map<String, dynamic>> simulations = [];
  simulations.add(business.calculateTax("yes", 2, 45000));
  simulations.add(business.calculateTax("no", 2, 45000));
  simulations.add(business.calculateTax("no", 1, 30000));
  // list of simulations
  log("-----------list-of-simulations");
  log(simulations);
  // deleting a simulation
  log("-----------deleting simulation no. 1");
  simulations.removeAt(1);
  log(simulations);
}

// JSON log
void log(Object? object) {
  const JsonEncoder encoder = JsonEncoder.withIndent('  ');
  print(encoder.convert(object));
}

Let’s comment on this code:

  • line 41: [final business = Business(taxAdminData);]: the [business] layer is instantiated only ONCE, immediately after retrieving the tax data from the server.
  • [simulations.add(business.calculateTax(...));]: The tax calculations no longer involve ANY network call—unlike main1.dart, where each calculation was a separate HTTP request.

The result of the execution is as follows:

"-----------init-session"
{
"action": "init-session",
"status": 700,
"response": "session started with type [json]"
}
"-----------authenticate-user"
{
"action": "authenticate-user",
"status": 200,
"response": "Authentication successful [admin, admin]"
}
"-----------get-admindata"
{
"action": "get-admindata",
"status": 1000,
"response": {
"limits": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"halfShareQfCeiling": 1551,
"singleIncomeCeilingForReduction": 21037,
"coupleIncomeCeilingForReduction": 42074,
"halfShareReductionValue": 3797,
"singleDiscountCeiling": 1196,
"coupleDiscountCeiling": 1970,
"coupleTaxCeilingForDiscount": 2627,
"singleTaxCeilingForDiscount": 1595,
"tenPercentAllowanceMax": 12502,
"tenPercentAllowanceMin": 437
}
}
"-----------calculate-tax x 3"
"-----------list-of-simulations"
[
{
"tax": 502,
"surcharge": 0,
"discount": 857,
"reduction": 126,
"rate": 0.14
},
{
"tax": 3250,
"surcharge": 370,
"discount": 0,
"reduction": 0,
"rate": 0.3
},
{
"tax": 1687,
"surcharge": 0,
"discount": 0,
"reduction": 0,
"rate": 0.14
}
]
"-----------deleting simulation no. 1"
[
{
"tax": 502,
"surcharge": 0,
"discount": 857,
"reduction": 126,
"rate": 0.14
},
{
"tax": 1687,
"surcharge": 0,
"discount": 0,
"reduction": 0,
"rate": 0.14
}
]

6.4. HTTP client 3

Image

6.4.1. Evolution of the toolkit since 2019

This third client uses the same [dao]/[business] pair as HTTP client 2, but is COMPILED FOR THE BROWSER instead of being run from the command line. As for TypeScript, this build requires webpack and babel, as well as polyfills ([core-js/stable], [regenerator-runtime/runtime]) so that modern code works in older browsers. On the Dart side, SDK provides its own web compiler ([dart compile js]): no polyfills are needed; [Future]/[async]/[await], records, patterns, etc., are compiled directly to the equivalent JavaScript, regardless of the target.

6.4.2. Compiling and Running the Project

From the [http client 3] folder:

The [webdev serve] development server is not provided by the Dart SDK: the [webdev] package must first be installed once, globally, using [dart pub global activate webdev].

This command installs the [webdev] executable in the global pub cache, a folder that is not automatically created in the Windows PATH directory. To add it there once and for all (as the current user), open a PowerShell terminal and run:

[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$env:LOCALAPPDATA\Pub\Cache\bin", "User")

You must then close and reopen the terminal (or VSCode) for the new PATH to take effect. Then, still in the [http client 3] folder:

dart pub get
webdev serve

In development, [webdev serve] launches a server that automatically recompiles with every change (hot reload): the terminal displays the URL to open in a browser (default: http://localhost:8080). For a production build, the [dart compile js web/main.dart -o web/main.dart.js] command remains available: the [web/index.html] page then loads [main.dart.js] to test the result—the same concept as the minimal entry point [src/index.ts] in the TypeScript course.

6.4.3. The [main3] script

6.4.4. script [main3]

// main3.dart
// Ports main3.ts.
//
// [WARNING, fundamental difference with TypeScript] main3.ts needed
// two polyfills (core-js/stable, regenerator-runtime) so that
// async/await and recent JavaScript features work on
// old browsers, once transpiled by babel/webpack. The
// Dart compiler for the web (`dart compile js`) needs NO
// polyfill of this kind: Future/async/await, Records, patterns...
// are compiled directly to equivalent JavaScript or WebAssembly,
// whatever the target - a direct advantage of compiling
// from a unified language and tooling, rather than transpiling
// "modern" JavaScript to "old" JavaScript.
import 'dart:convert';
import 'package:http/browser_client.dart';
import 'dao3.dart';
import 'business.dart';

Future<void> main() async {
  // browser-specific HTTP client, with withCredentials=true -
  // the Dart equivalent of "axios.defaults.withCredentials = true"
  final client = BrowserClient()..withCredentials = true;
  final dao = Dao3(client, 'http://localhost:3000');

  Map<String, dynamic> taxAdminData;
  try {
    log("-----------init-session");
    Map<String, dynamic> response = await dao.initSession();
    log(response);
    if (response['status'] != 700) {
      throw Exception(jsonEncode(response['response']));
    }
    log("-----------authenticate-user");
    response = await dao.authenticateUser("admin", "admin");
    log(response);
    if (response['status'] != 200) {
      throw Exception(jsonEncode(response['response']));
    }
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    if (response['status'] != 1000) {
      throw Exception(jsonEncode(response['response']));
    }
    taxAdminData = response['response'] as Map<String, dynamic>;
  } catch (error) {
    print("error=$error");
    return;
  } finally {
    client.close();
  }

  // instantiating the [business] layer
  final business = Business(taxAdminData);

  log("-----------calculate-tax x 3");
  final List<Map<String, dynamic>> simulations = [];
  simulations.add(business.calculateTax("yes", 2, 45000));
  simulations.add(business.calculateTax("no", 2, 45000));
  simulations.add(business.calculateTax("no", 1, 30000));
  log("-----------list-of-simulations");
  log(simulations);
  log("-----------deleting simulation no. 1");
  simulations.removeAt(1);
  log(simulations);
}

// JSON log
void log(Object? object) {
  const JsonEncoder encoder = JsonEncoder.withIndent('  ');
  print(encoder.convert(object));
}

Let’s comment on this code:

  • line 22: [final client = BrowserClient()..withCredentials = true;]: [BrowserClient] ([package:http/browser_client.dart]) only exists when the code is compiled for the web; its option [withCredentials] is the Dart equivalent of [axios.defaults.withCredentials = true]—the BROWSER then handles the session cookie on its own.
  • The Dart web compiler does not require ANY polyfill (unlike main3.ts): [Future]/[async]/[await], and recent language features are compiled directly—a direct benefit of starting with a unified language and toolset rather than transpiling from modern JavaScript to legacy JavaScript.

6.4.5. The [Dao3]

6.4.6. [dao3] script

// dao3.dart
// Ports tax client/http client 3/src/Dao3.ts.
//
// Difference with dao2.dart: this file is meant to be compiled for the
// BROWSER (with `dart compile js`, see the README of this folder), not
// run on the CLI with `dart run`. In a browser, the SESSION COOKIE
// is handled automatically by the browser itself (as for any
// web page) as soon as sending them is allowed - so there is no longer
// any need for the MANUAL cookie tracking done by hand in dao1.dart/dao2.dart
// (exactly the reasoning, already present, that allowed Dao3.ts to
// remove all the cookie-handling code of Dao2.ts).
//
// [mapping with TypeScript] "axios.defaults.withCredentials = true"
// (set in main3.dart, not here) corresponds, on the Dart [http] package side, to
// the [withCredentials] option of the web-specific client,
// [BrowserClient] (package:http/browser_client.dart) - a class that
// exists ONLY when the code is compiled for the web, unlike
// http.Client, which works everywhere.
import 'dart:convert';
import 'package:http/http.dart' as http;

class Dao3 {
  final http.Client _client;
  final String _baseUrl;

  Dao3(this._client, this._baseUrl);

  Future<dynamic> initSession() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'init-session', 'type': 'json'},
    );
  }

  Future<dynamic> authenticateUser(String user, String password) {
    return _getRemoteData(
      'main.php',
      method: 'POST',
      queryParameters: {'action': 'authenticate-user'},
      body: {'user': user, 'password': password},
    );
  }

  Future<dynamic> getAdminData() {
    return _getRemoteData(
      'main.php',
      method: 'GET',
      queryParameters: {'action': 'get-admindata'},
    );
  }

  Future<dynamic> _getRemoteData(
    String path, {
    required String method,
    Map<String, dynamic>? queryParameters,
    Map<String, String>? body,
  }) async {
    final uri = Uri.parse('$_baseUrl/$path').replace(
      queryParameters: queryParameters?.map((k, v) => MapEntry(k, '$v')),
    );
    final http.Response response;
    if (method == 'POST') {
      response = await _client.post(uri, body: body);
    } else {
      response = await _client.get(uri);
    }
    // [difference with the CLI client] no manual reading of the set-cookie
    // header here: in a browser, response.headers does not even expose it
    // (header protected by the browser, invisible to the
    // JavaScript/Dart code, precisely because it is already handled by itself)
    return jsonDecode(response.body);
  }
}

Let’s comment on this code:

  • NO more manual tracking of the session cookie (unlike [Dao1]/[Dao2]): In a browser, the session cookie is managed automatically by the browser itself, provided that sending cookies is allowed ([withCredentials], set in main3.dart).
  • [response.headers] no longer even exposes the [set-cookie] header in a browser: a protected header, invisible to the client-side JavaScript/Dart code, precisely because it is already managed on its own.

6.4.7. Compilation and Execution

See the [Compiling and Running the Project] section above: the same command [dart compile js] compiles [web/main.dart], which delegates to [main3.dart].

Start the development server to run the Http3 client web application:

1
2
3
4
5
PS C:\Data\st-2026\dev\dart-sept-2026-scripts\dart-fundamentals\tax-client\dart-fundamentals\tax client\http client 3> webdev serve
[INFO] Serving `web` on http://127.0.0.1:8080
[INFO] ------------------------------------------------------------------------------------
[INFO] Injected debugging metadata for entrypoint at http://127.0.0.1:8080/main.dart.bootstr
[INFO] Injected debugging metadata for entrypoint at http://localhost:8080/main.dart.bootstrap.js

The result of the execution is as follows:

Image

  • In [1], open a browser and request URL [http://localhost:8080];
  • in [2], the text displayed by the requested page;
  • in [3], the result of the execution can be found in the browser console (F12, then the Console option). We get the same result as before, but this time in a browser;

6.5. HTML client

Image

6.5.1. Solution Architecture

This fourth and final client adds a true HTML interface on top of HTTP client 3: the [dao] ([Dao3]) and [business] ([Business]) layers are REUSED WITHOUT MODIFICATION, and two new layers are added—VIEWS (DOM build) and CONTROLLER (orchestration). The project is organized into two folders, [web/] (entry point, HTML, CSS) and [lib/] (shared code), following the convention of a standard Dart web project—see README.md in this folder for details on this convention.

6.5.2. The HTML template: the [web/index.html] file

Copied exactly from [src/index.html] (same classes as CSS: [banner], [menu], [content], [section-title], [field-row], [result], [error], [simulations]...), with one small difference: the script being loaded is [main.dart.js] (a FIXED name set by the Dart web tool), whereas [index.html] from Views.ts loaded a Webpack bundle with a HASHED name ([main.a44f6e1c12ab338daedc.js]).

6.5.3. The modified [webpack.config.js] configuration

No equivalent is needed on the Dart side: no bundler configuration file needs to be written. [web/main.dart], by virtue of its sole POSITION in the directory tree (directly under [web/]), plays the role that the [entry: './src/main3.ts'] line played for [webpack.config.js]—a naming and location convention replaces an entire configuration file.

6.5.4. The layers [dao] and [business]: two files reused without modification

[lib/dao3.dart] and [lib/business.dart] are EXACT copies of the files of the same name from HTTP client 3 - the same reuse, without any modifications, as that performed by Views.ts and AppHtml.ts on [Dao3.ts] and [Business.ts].

6.5.5. The [views] layer: the [Views.ts]

6.5.6. [simulation, views] scripts

The views layer is supported by THREE files: simulation.dart (the displayed data), views.dart (the construction of DOM), and app_html.dart (the controller, discussed in the next section). Let’s start with simulation.dart:

// simulation.dart
// The result of a tax calculation, as returned by business.dart, completed
// with the entered parameters. Ports the [Simulation] interface of Views.ts.
//
// [NEW, Dart 3] here we use a named RECORD (see
// dart-fundamentals/dart3/dart3_01_records.dart) rather than a class: a
// simple type alias [typedef], with no constructor or separate file to
// write, for data that is just a plain "bag" of fields, with no
// associated behaviour - exactly the spirit of the original TypeScript
// interface.
//
// [NOTE on field names] unlike TypeScript/JavaScript, where
// an identifier may contain accented letters (used throughout the
// original French course), Dart restricts
// identifiers to ASCII letters: accents are only possible in
// CHARACTER STRINGS, never in a name of a
// variable, field, class or record. In any case, this course
// uses plain English ASCII names everywhere (married, tax, discount...):
// the record fields below have the same names as the Map keys of
// business.dart.
typedef Simulation = ({
  String married,
  int children,
  num salary,
  num tax,
  num surcharge,
  num discount,
  num reduction,
  num rate,
});

Let’s break down this code:

  • lines 21–24: [typedef Simulation = ({String married, int children, num salary, ...});]: whereas Views.ts declared an INTERFACE [Simulation], this file uses a Dart 3 NAMED RECORD (see dart3_01_records.dart)—a simple type alias, with no constructor or separate file, for purely descriptive data.
  • Field names are plain ASCII ([married], [tax], [discount]...): Dart IDENTIFIERS are restricted to ASCII letters, unlike TypeScript/JavaScript, where accented names such as [marié]/[impôt] (used in the original French course) are valid field names.

[lib/views.dart], which is composed of DOM and [package:web]:

// views.dart
// Ports Views.ts: building the views (DOM) of the application - authentication
// view, tax calculation view, list of simulations view. This
// module makes no network call: it builds the DOM and delegates the
// user's actions to callback functions supplied by the
// controller (see app_html.dart).
//
// [technical choice] here we use [package:web] (pub.dev/packages/web),
// the modern replacement of the old dart:html: it exposes a DOM API very
// close to the standard DOM used by Views.ts (document.createElement,
// .textContent, .addEventListener...). A surface difference to
// know: a Dart event handler (a plain function) must
// be converted into a JavaScript function with [.toJS] (extension of
// dart:js_interop) before being passed to [addEventListener] - the "bridge"
// between the Dart world and the JavaScript world, made explicit, where
// TypeScript needs no conversion (everything there is already
// JavaScript).
import 'dart:js_interop';
import 'package:web/web.dart' as web;
import 'simulation.dart';

// a generic decoration (not the photo of the original PHP server)
const String _logoSvg = '''
<svg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
  <circle cx="30" cy="30" r="29" fill="#eef1ec" stroke="#ccc" />
  <g fill="none" stroke="#8a9a7a" stroke-width="2">
    <path d="M30 46 C30 32 22 28 17 19" />
    <path d="M30 46 C30 30 39 26 44 17" />
    <path d="M30 46 C30 37 30 30 30 21" />
  </g>
  <g fill="#b9c9a6">
    <circle cx="17" cy="19" r="4.5" />
    <circle cx="44" cy="17" r="4.5" />
    <circle cx="30" cy="19" r="4.5" />
  </g>
</svg>''';

// callbacks of the [authentication] view - [difference with TypeScript]
// a TypeScript interface of callbacks naturally translates into a
// RECORD of functions in Dart (see simulation.dart): lighter than a
// class, for a simple group of functions
typedef AuthenticationCallbacks = ({void Function(String user, String password) onSubmit});

typedef CalculationCallbacks = ({
  void Function(String married, int children, int salary) onSubmit,
  void Function() onList,
  void Function() onEndSession,
});

typedef ListCallbacks = ({
  void Function(int number) onDelete,
  void Function() onCalculation,
  void Function() onEndSession,
});

class Views {
  final web.HTMLElement root;
  late web.HTMLDivElement _banner;
  late web.HTMLDivElement _menu;
  late web.HTMLDivElement _content;

  // builds the fixed structure of the page (banner + menu/content area)
  // inside the [root] element
  Views(this.root) {
    // [WARNING] unlike ordinary DOM properties (textContent,
    // value...), the [innerHTML] setter of package:web expects a [JSAny], not
    // a raw Dart [String] - a peculiarity of its generation linked to the
    // WebIDL attribute [LegacyNullToEmptyString] of the standard DOM: so we
    // must always convert with [.toJS].
    root.innerHTML = ''.toJS;

    _banner = _div()..className = 'banner';
    final logo = _div()..className = 'logo';
    logo.innerHTML = _logoSvg.toJS;
    final title = web.document.createElement('h1') as web.HTMLHeadingElement;
    title.textContent = 'Calculate your tax';
    _banner
      ..appendChild(logo)
      ..appendChild(title);

    final body = _div()..className = 'page-body';
    _menu = _div()..className = 'menu';
    _content = _div()..className = 'content';
    body
      ..appendChild(_menu)
      ..appendChild(_content);

    root
      ..appendChild(_banner)
      ..appendChild(body);
  }

  // --------------------------------------------------------------------
  // DOM-building tools
  // --------------------------------------------------------------------

  web.HTMLDivElement _div() => web.document.createElement('div') as web.HTMLDivElement;

  void _clearMenu() {
    _menu.innerHTML = ''.toJS;
    _menu.style.display = 'none';
    _content.classList.add('full-width');
  }

  web.HTMLAnchorElement _menuLink(String text, void Function() onClick) {
    final a = web.document.createElement('a') as web.HTMLAnchorElement;
    a.textContent = text;
    a.addEventListener('click', ((web.Event event) => onClick()).toJS);
    return a;
  }

  void _showMenu(List<web.HTMLAnchorElement> links) {
    _menu.innerHTML = ''.toJS;
    _menu.style.display = '';
    _content.classList.remove('full-width');
    for (final link in links) {
      _menu.appendChild(link);
    }
  }

  web.HTMLElement? _errorBanner(String? message) {
    if (message == null) return null;
    final div = _div()..className = 'error';
    div.textContent = message;
    return div;
  }

  // a form row [label + text/password/number field]
  (web.HTMLDivElement row, web.HTMLInputElement input) _fieldRow(
    String labelText,
    String type,
    String initialValue, {
    String? help,
  }) {
    final row = _div()..className = 'field-row';
    final label = web.document.createElement('label') as web.HTMLLabelElement;
    label.textContent = labelText;
    final field = _div()..className = 'field';
    final input = web.document.createElement('input') as web.HTMLInputElement;
    input.type = type;
    input.value = initialValue;
    if (type == 'number') {
      input.min = '0';
    }
    field.appendChild(input);
    if (help != null) {
      final div = _div()..className = 'help';
      div.textContent = help;
      field.appendChild(div);
    }
    row
      ..appendChild(label)
      ..appendChild(field);
    return (row, input);
  }

  // --------------------------------------------------------------------
  // [authentication] view
  // --------------------------------------------------------------------
  void authenticationView(String? error, AuthenticationCallbacks callbacks) {
    _clearMenu();
    _content.innerHTML = ''.toJS;

    final title = _div()
      ..className = 'section-title'
      ..textContent = 'Please authenticate';
    _content.appendChild(title);

    final banner = _errorBanner(error);
    if (banner != null) _content.appendChild(banner);

    final userField = _fieldRow("Username", 'text', 'admin');
    final passwordField = _fieldRow('Password', 'password', '');
    _content
      ..appendChild(userField.$1)
      ..appendChild(passwordField.$1);

    final button = web.document.createElement('button') as web.HTMLButtonElement;
    button.textContent = 'Submit';
    button.addEventListener(
      'click',
      ((web.Event event) {
        callbacks.onSubmit(userField.$2.value.trim(), passwordField.$2.value);
      }).toJS,
    );
    _content.appendChild(button);
  }

  // --------------------------------------------------------------------
  // [tax calculation] view
  // --------------------------------------------------------------------
  void calculationView(Simulation? result, String? error, CalculationCallbacks callbacks) {
    _showMenu([
      _menuLink('List of simulations', callbacks.onList),
      _menuLink('End of session', callbacks.onEndSession),
    ]);

    _content.innerHTML = ''.toJS;

    final title = _div()
      ..className = 'section-title'
      ..textContent = 'Fill in the form below and then submit it';
    _content.appendChild(title);

    final banner = _errorBanner(error);
    if (banner != null) _content.appendChild(banner);

    // row of the [married] radios
    final marriedRow = _div()..className = 'field-row';
    final marriedLabel = web.document.createElement('label') as web.HTMLLabelElement;
    marriedLabel.textContent = 'Are you married or in a civil partnership?';
    final marriedField = _div()..className = 'field radios';

    final radioYes = web.document.createElement('input') as web.HTMLInputElement;
    radioYes.type = 'radio';
    radioYes.name = 'married';
    radioYes.value = 'yes';
    radioYes.checked = true;
    final labelYes = web.document.createElement('label') as web.HTMLLabelElement;
    labelYes.appendChild(radioYes);
    labelYes.appendChild(web.document.createTextNode(' Yes'));

    final radioNo = web.document.createElement('input') as web.HTMLInputElement;
    radioNo.type = 'radio';
    radioNo.name = 'married';
    radioNo.value = 'no';
    final labelNo = web.document.createElement('label') as web.HTMLLabelElement;
    labelNo.appendChild(radioNo);
    labelNo.appendChild(web.document.createTextNode(' No'));

    marriedField
      ..appendChild(labelYes)
      ..appendChild(labelNo);
    marriedRow
      ..appendChild(marriedLabel)
      ..appendChild(marriedField);
    _content.appendChild(marriedRow);

    final childrenField = _fieldRow("Number of dependent children", 'number', '0');
    _content.appendChild(childrenField.$1);

    final salaryField = _fieldRow(
      'Annual salary',
      'number',
      '',
      help: "Round down to the nearest euro",
    );
    _content.appendChild(salaryField.$1);

    final button = web.document.createElement('button') as web.HTMLButtonElement;
    button.textContent = 'Submit';
    button.addEventListener(
      'click',
      ((web.Event event) {
        final married = radioYes.checked ? 'yes' : 'no';
        final children = int.tryParse(childrenField.$2.value) ?? 0;
        final salary = int.tryParse(salaryField.$2.value) ?? 0;
        callbacks.onSubmit(married, children, salary);
      }).toJS,
    );
    _content.appendChild(button);

    // result of the last simulation (green box), if there is one
    if (result != null) {
      final box = _div()..className = 'result';
      final ratePercent = (result.rate * 100).round();
      box.innerHTML = ("Tax amount: ${result.tax} euros<br>"
              "Discount: ${result.discount} euros<br>"
              "Reduction: ${result.reduction} euros<br>"
              "Surcharge: ${result.surcharge} euros<br>"
              "Tax rate: $ratePercent %")
          .toJS;
      _content.appendChild(box);
    }
  }

  // --------------------------------------------------------------------
  // [list of simulations] view
  // --------------------------------------------------------------------
  void listView(List<Simulation> simulations, String? error, ListCallbacks callbacks) {
    _showMenu([
      _menuLink("Tax calculation", callbacks.onCalculation),
      _menuLink('End of session', callbacks.onEndSession),
    ]);

    _content.innerHTML = ''.toJS;

    final title = _div()
      ..className = 'section-title'
      ..textContent = 'List of your simulations';
    _content.appendChild(title);

    final banner = _errorBanner(error);
    if (banner != null) _content.appendChild(banner);

    if (simulations.isEmpty) {
      final empty = _div()
        ..className = 'simulations-empty'
        ..textContent = 'No simulation recorded.';
      _content.appendChild(empty);
      return;
    }

    final table = web.document.createElement('table') as web.HTMLTableElement;
    table.className = 'simulations';
    final thead = web.document.createElement('thead') as web.HTMLTableSectionElement;
    thead.innerHTML = ("<tr><th>#</th><th>Married</th><th>Number of children</th>"
            "<th>Annual salary</th><th>Tax amount</th><th>Surcharge</th>"
            "<th>Discount</th><th>Reduction</th><th>Rate</th><th></th></tr>")
        .toJS;
    table.appendChild(thead);

    final tbody = web.document.createElement('tbody') as web.HTMLTableSectionElement;
    for (var number = 0; number < simulations.length; number++) {
      final simulation = simulations[number];
      final tr = web.document.createElement('tr') as web.HTMLTableRowElement;
      final ratePercent = (simulation.rate * 100).round();
      tr.innerHTML = ("<td>$number</td>"
              "<td>${simulation.married}</td>"
              "<td>${simulation.children}</td>"
              "<td>${simulation.salary}</td>"
              "<td>${simulation.tax}</td>"
              "<td>${simulation.surcharge}</td>"
              "<td>${simulation.discount}</td>"
              "<td>${simulation.reduction}</td>"
              "<td>$ratePercent %</td>")
          .toJS;
      final tdDelete = web.document.createElement('td') as web.HTMLTableCellElement;
      final deleteLink = web.document.createElement('a') as web.HTMLAnchorElement;
      deleteLink.className = 'delete';
      deleteLink.textContent = 'Delete';
      deleteLink.addEventListener(
        'click',
        ((web.Event event) => callbacks.onDelete(number)).toJS,
      );
      tdDelete.appendChild(deleteLink);
      tr.appendChild(tdDelete);
      tbody.appendChild(tr);
    }
    table.appendChild(tbody);
    _content.appendChild(table);
  }
}

Let’s break down this code:

  • line 19: [import 'package:web/web.dart' as web;]: the modern replacement for the old [dart:html], with a API and DOM very similar to the standard DOM used by Views.ts ([document.createElement], [.textContent], [.addEventListener]...).
  • Lines 44–45: [typedef CalculationCallbacks = ({void Function(...) onSubmit, ...});]: A TypeScript callback interface naturally translates to a RECORD OF FUNCTIONS in Dart 3—lighter than a class for a simple group of functions.
  • Line 108: [a.addEventListener('click', ((web.Event event) { ... }).toJS);]: A Dart event handler (a simple function) must be converted to the function JavaScript using [.toJS] (an extension of [dart:js_interop]) before being passed to [addEventListener] - the “bridge” between the Dart world and the JavaScript world is made explicit, whereas TypeScript requires no conversion.
  • line 141: [input.value = initialValue;], [title.textContent = '...';] (line 76): most string-type properties remain simple Dart strings, with no conversion required. [innerHTML] is the EXCEPTION: [dart analyze] signals this if a [String] is assigned to it directly—its setter expects a [JSAny] (a peculiarity of its generation, related to the WebIDL [LegacyNullToEmptyString] attribute of the standard DOM)—hence [root.innerHTML = ''.toJS;]: an additional [.toJS] conversion, beyond the functions alone, which must be kept in mind whenever handling [innerHTML].
  • line 129: [(web.HTMLDivElement row, web.HTMLInputElement input) _fieldRow(...)]: a positional RECORD as the return type, to return two DOM elements at once without an intermediate class—yet another practical use of Dart 3 records.

6.5.7. The controller: the [AppHtml.ts] file

6.5.8. [app_html] script

// app_html.dart
// Ports AppHtml.ts: the controller of the HTML application. It orchestrates the
// calls to the server ([dao] layer, dao3.dart), the calculations
// ([business] layer, business.dart) and the display of the views ([views] layer,
// views.dart).
//
// Like main3.dart, this client calls the NestJS server for only three
// actions: [init-session], [authenticate-user] and [get-admindata].
// Once the tax data (rates, ceilings...) has been retrieved with
// [get-admindata], it is the [business] layer that calculates the tax in the
// browser, with no new network call. The list of simulations is also
// kept only in the browser (field [simulations]),
// exactly as main3.dart does with its local variable
// [simulations] (`simulations.add(...)`, `simulations.removeAt(...)`).
import 'dart:js_interop';
import 'package:http/browser_client.dart';
import 'dao3.dart';
import 'business.dart';
import 'simulation.dart';
import 'views.dart';
import 'package:web/web.dart' as web;

class AppHtml {
  final Dao3 dao;
  final Views views;
  // [business] layer: only exists once the tax data has been retrieved
  // (after a successful authentication) - see [_initBusiness]. A
  // NULLABLE field (Business?), not a late field ([late]): unlike
  // Views.ts where [business?: Business] simply stays "undefined" as long as it
  // is not assigned, a Dart [late] field would throw an error if it were
  // READ before assignment (which is precisely what [_endSession] does when
  // resetting it) - so the nullable type is the right choice here, not
  // just a matter of style.
  Business? _business;
  // list of the simulations already calculated, kept in the browser
  List<Simulation> _simulations = [];

  // [difference with TypeScript] AppHtml.ts configures a GLOBAL axios
  // instance (axios.defaults.timeout/baseURL/withCredentials) then hands it
  // to the Dao. Here, [BrowserClient] (package:http/browser_client.dart, already
  // met in main3.dart) carries the equivalent of
  // "axios.defaults.withCredentials = true"; there is no direct
  // equivalent of a "global timeout" on http.Client - it would be applied case
  // by case with `.timeout(Duration(...))` on each call, as in the
  // [http] chapter (dart_fetch_01.dart).
  AppHtml()
      : dao = Dao3(
          BrowserClient()..withCredentials = true,
          'http://localhost:3000',
        ),
        views = Views(web.document.getElementById('app') as web.HTMLElement);

  // entry point of the application
  Future<void> start() async {
    try {
      // init session - mandatory before any other action (see main.php)
      final response = await dao.initSession() as Map<String, dynamic>;
      if (response['status'] != 700) {
        views.authenticationView(
          _formatError(response['response']),
          (onSubmit: (user, password) => _authenticate(user, password)),
        );
        return;
      }
    } catch (error) {
      views.authenticationView(
        "Unable to contact the server ($error)",
        (onSubmit: (user, password) => _authenticate(user, password)),
      );
      return;
    }
    // session initialized: we display the authentication
    _showAuthentication();
  }

  // --------------------------------------------------------------------
  // [authentication] view
  // --------------------------------------------------------------------
  void _showAuthentication([String? error]) {
    views.authenticationView(
      error,
      (onSubmit: (user, password) => _authenticate(user, password)),
    );
  }

  Future<void> _authenticate(String user, String password) async {
    try {
      final response = await dao.authenticateUser(user, password)
          as Map<String, dynamic>;
      if (response['status'] == 200) {
        // successful authentication: as in main3.dart, we now retrieve,
        // once and for all, the tax data
        // needed by the [business] layer
        await _initBusiness();
      } else {
        // failure (wrong login/password, or missing parameters)
        _showAuthentication(_formatError(response['response']));
      }
    } catch (error) {
      _showAuthentication("Unable to contact the server ($error)");
    }
  }

  // retrieves the tax administration data and instantiates the
  // [business] layer with this data - a single server call for the whole
  // session, exactly like main3.dart
  Future<void> _initBusiness() async {
    try {
      final response = await dao.getAdminData() as Map<String, dynamic>;
      if (response['status'] == 1000) {
        _business = Business(response['response'] as Map<String, dynamic>);
        // new work session: we start again from an empty list
        _simulations = [];
        _showCalculation();
      } else {
        _showAuthentication(_formatError(response['response']));
      }
    } catch (error) {
      _showAuthentication("Unable to contact the server ($error)");
    }
  }

  // --------------------------------------------------------------------
  // [tax calculation] view
  // --------------------------------------------------------------------
  void _showCalculation([Simulation? result, String? error]) {
    views.calculationView(
      result,
      error,
      (
        onSubmit: (married, children, salary) =>
            _calculate(married, children, salary),
        onList: () => _showList(),
        onEndSession: () => _endSession(),
      ),
    );
  }

  // tax calculation done entirely in the browser ([business]
  // layer): no call to the server, unlike the first version
  // of this client (http client 1 / http client 2)
  void _calculate(String married, int children, int salary) {
    // _business has necessarily been initialized: we only reach the [calculation] view
    // after _initBusiness (so the [!] is safe here, like the TypeScript
    // [!] on [this.business!])
    final result = _business!.calculateTax(married, children, salary);
    // [difference with TypeScript] Views.ts builds the simulation with a
    // simple object spread: `{ married, children, salary, ...result }`.
    // Here, [Simulation] is a RECORD (simulation.dart), not a Map: a
    // record cannot be "completed" by spreading a Map, and
    // its fields are typed (num, int...) whereas
    // [result], returned by business.dart, is an untyped Map whose keys
    // merely HAVE the same names ('tax', 'surcharge'..., see business.dart). So we
    // rebuild the record field by field - a little more verbose
    // than a spread, but entirely checked by the compiler (a forgotten
    // or wrongly typed field is a compilation error, not a surprise at
    // runtime).
    final Simulation simulation = (
      married: married,
      children: children,
      salary: salary,
      tax: result['tax'] as num,
      surcharge: result['surcharge'] as num,
      discount: result['discount'] as num,
      reduction: result['reduction'] as num,
      rate: result['rate'] as num,
    );
    // storage in the browser (like `simulations.add(...)` in
    // main3.dart)
    _simulations.add(simulation);
    _showCalculation(simulation);
  }

  // --------------------------------------------------------------------
  // [list of simulations] view
  // --------------------------------------------------------------------
  // no more server calls here: the list is already entirely in
  // [_simulations]
  void _showList([String? error]) {
    views.listView(
      _simulations,
      error,
      (
        onDelete: (number) => _deleteSimulation(number),
        onCalculation: () => _showCalculation(),
        onEndSession: () => _endSession(),
      ),
    );
  }

  void _deleteSimulation(int number) {
    // local deletion from [_simulations], like
    // `simulations.removeAt(number)` in main3.dart - still no
    // server call
    _simulations.removeAt(number);
    _showList();
  }

  // --------------------------------------------------------------------
  // end of session
  // --------------------------------------------------------------------
  void _endSession() {
    // end of session on the client side only: we forget the [business] layer
    // (and therefore the tax data) and the simulations already calculated, then
    // we go back to the authentication screen. This client does not need to
    // call the server for this.
    _business = null;
    _simulations = [];
    _showAuthentication();
  }

  // --------------------------------------------------------------------
  // formats the [response] field of an error response from the server, which
  // is either a string or a list of strings (see PHP controllers)
  // --------------------------------------------------------------------
  String _formatError(Object? response) {
    if (response is List) {
      return response.join(' ; ');
    }
    return '$response';
  }
}

// execution - [difference with TypeScript] the event handler
// passed to addEventListener must be converted into a JavaScript function with
// [.toJS] (see views.dart); this is just the "entry point" version of
// the same rule.
void main() {
  web.window.addEventListener(
    'DOMContentLoaded',
    ((web.Event event) {
      AppHtml().start();
    }).toJS,
  );
}

Let’s comment on this code:

  • lines 46–51: [AppHtml() : dao = Dao3(BrowserClient()..withCredentials = true, ...), views = Views(...)]: constructing the DAO and the VIEWS in an INITIALIZER LIST - Same concept as the TypeScript constructor, using the Dart-specific [: field1 = ..., field2 = ...] syntax.
  • Line 34: [Business? _business;]: a NULLABLE field, where AppHtml.ts previously used [business?: Business] - the same concept (“not yet initialized”), using the same [!] operator to confirm that it is indeed populated at a specific point in the code.
  • Lines 158–162: [final Simulation simulation = (married: married, ..., tax: result['tax'] as num, ...);]: unlike the simple spread [{ married, children, salary, ...result }] from AppHtml.ts, a RECORD cannot be completed by a spread from a [Map] - the record is therefore reconstructed FIELD BY FIELD, which is slightly more verbose but fully validated by the compiler.
  • Lines 229–233: [web.window.addEventListener('DOMContentLoaded', ((web.Event event) { AppHtml().start(); }).toJS);]: the same entry point as AppHtml.ts, with the same callback conversion ([.toJS]) as in views.dart.

6.5.9. Compilation and Execution

See README.md in this folder: [dart pub get] followed by [webdev serve] for development (including hot reloading), or [dart compile js web/main.dart -o web/main.dart.js] for a production build—the Dart equivalent of [npm run build], without any prior bundler configuration steps.

If the [webdev serve] server is still running for the [http client 3] web application, you must stop it (Ctrl-C in the terminal that launched it). Then, in a terminal within the [html client] directory:

dart pub get
webdev serve

Line 2 starts the web server that will serve the HTML client application:

Image

  • In [1], in a browser, request URL [http://localhost:8080];
  • to [2-3], enter the credentials admin / admin;
  • In [4], submit the page;

Image

  • In [2], fill in the fields;
  • In [3], submit the page;

Image

The tax amount is displayed in [1]. Run several simulations, then request the list in [2];

Image

  • In [1], delete a simulation;

Image

  • in [1], end the session;

Image

6.6. Conclusion

The four clients in this chapter illustrate, with Dart, exactly the same progression as with TypeScript: from server-side processing to client-side processing, then from the CLI script to the full web client. The key differences to note are the manual tracking of the session cookie (necessary in CLI, unnecessary in a browser), the [.toJS] conversion enforced by [dart:js_interop] to bridge the Dart/JavaScript boundary in views, and the complete absence of polyfills during web compilation—a clear advantage of the Dart toolchain over the webpack/babel pipeline.