Skip to content

Latest commit

 

History

History
378 lines (276 loc) · 19.2 KB

README.md

File metadata and controls

378 lines (276 loc) · 19.2 KB

Wallet App (Flutter)

This README explains how to configure the wallet app. It also contains the currently supported mock scenarios, describes our conventions and project structure and finally offers information on the design decisions we made in the Architecture section.

Table of contents

Running the App

Environment Setup

To run the app you need to configure Flutter, Rust, the Android SDK and the iOS SDK. See the project root's README.md for instructions on how to do so.

Running

After setting up your environment, launch an Android emulator or the iOS simulator and execute:

  • flutter run or
  • fvm flutter run when using Flutter Version Manager (FVM)

Note that when using FVM, all Flutter commands below should be prefixed with fvm.

Building

The easiest way to build the app locally is to use fastlane. To install use: bundle install.

iOS

To build the iOS app use: bundle exec fastlane ios build. Note that password for fastlane match repository will be requested while building, this repository is not public. Alternatively you can build the app using flutter build ios, which will rely on your own certificate.

Android

To build the Android app use: bundle exec fastlane android build. Note that this requires you to add a local signing key to the project:

  1. Get the nl-wallet-android-local-signing-key secrets from the secrets repository.
  2. Move local-keystore.jks file into the wallet_app/android/keystore folder.
  3. Move the key.properties file into the wallet_app/android folder.
  4. That's it! Building release builds, e.g. with bundle exec fastlane android build should now work.

App Configuration

We are currently maintaining two 'flavours' of the app, a mock and an core version.

Mock

The mocked version is what is used to demonstrate and verify potential use cases of the app. This version is preliminary used for usability research and contains many mock (fake) scenarios that can be triggered using the QR codes or Deeplinks provided below. This version of the app does not require internet and thus does not require any server to be running.

Running the mock variant of the app is straight forward, as it's currently the default behaviour when using flutter run. But to explicitly run a mock build use:

flutter run --dart-define=MOCK_REPOSITORIES=true

Core

The 'core' version is of the app is what will (work in progress) grow into the functioning MVP. It relies heavily on all the logic implemented in the wallet_core, which is the component that handles all the business logic (like storage, network and validation) needed to achieve the app's functionalities.

A sample command to run the core version of the app is provided below.

flutter run --dart-define=MOCK_REPOSITORIES=false --dart-define ENV_CONFIGURATION=true --dart-define UL_HOSTNAME={hostname}

However, since the core version relies heavily on communication with other services, we also provide scripts to configure the complete development environment. Please refer to scripts, and more specifically the setup-devenv.sh and start-devenv.sh files.

File Structure

Assets

All files used by the project (like images, fonts, etc.), go into the assets/ directory and their appropriate sub-directories.

Note; the assets/non-free/images/ directory contains resolution-aware images.

Copyright note: place all non free (copyrighted) assets used under the non-free/ directory inside the appropriate asset sub-directory.

Localization

Text localization is enabled; currently supporting English & Dutch, with English set as primary ( fallback) language. Localized messages are generated based on ARB files found in the lib/l10n directory.

To support additional languages, please visit the tutorial on Internationalizing Flutter apps.

Internally, this project uses the commercial Lokalise service to manage translations. This service is currently not accessible for external contributors. For contributors with access, please see documentation/lokalise.md for documentation.

Deeplink Scenarios

Below you can find the deeplinks that can be used to trigger the supported mock scenarios. On Android, the scenarios can be triggered from the command line by using adb shell am start -a android.intent.action.VIEW -d "{deeplink}". On iOS, the scenarios are triggered with the command xcrun simctl openurl booted '{deeplink}'. Note that the deeplinks only work on debug builds. For mock production builds you can generate a QR code from the content and scan these using the app. Pre-generated QR codes are also available, and can be found here.

Issuance Scenarios

Scenario Content Deeplink
Driving License {"id":"DRIVING_LICENSE","type":"issue"} Issue driving license
Extended Driving License {"id":"DRIVING_LICENSE_RENEWED","type":"issue"} Issue extended driving license
Diploma {"id":"DIPLOMA_1","type":"issue"} Issue diploma
Health Insurance {"id":"HEALTH_INSURANCE","type":"issue"} Issue health insurance
VOG {"id":"VOG","type":"issue"} Issue VOG
Multiple Diplomas {"id":"MULTI_DIPLOMA","type":"issue"} Issue mutiple diplomas

Disclosure Scenarios

Scenario Content Deeplink
Job Application {"id":"JOB_APPLICATION","type":"verify"} Disclose for job application
Bar {"id":"BAR","type":"verify"} Disclose for bar
Marketplace Login {"id":"MARKETPLACE_LOGIN","type":"verify"} Login to marketplace
Car Rental {"id":"CAR_RENTAL","type":"verify"} Disclose for car rental
First Aid {"id":"FIRST_AID","type":"verify"} Disclose for first aid
Parking Permit {"id":"PARKING_PERMIT","type":"verify"} Disclose for parking permit
Open Bank Account {"id":"OPEN_BANK_ACCOUNT","type":"verify"} Disclose to open bank account
Provide Contract Details {"id":"PROVIDE_CONTRACT_DETAILS","type":"verify"} Disclose to provide contract details
Create MonkeyBike Account {"id":"CREATE_MB_ACCOUNT","type":"verify"} Disclose to create MB account
Pharmacy {"id":"PHARMACY","type":"verify"} Disclose for pharmacy
Amsterdam Login {"id":"AMSTERDAM_LOGIN","type":"verify"} Login to Amsterdam

Sign Scenarios

Scenario Content Deeplink
Rental Agreement {"id":"RENTAL_AGREEMENT","type":"sign"} Sign rental agreement

E2E Test Scenarios

Scenario Deep dive link Explanation
Skip (setup) to home Skip setup Use on clean app startup; to setup wallet with mock data and jump straight to the home (a.k.a. cards overview) screen

Conventions

This section specifies some of the conventions we use to format our .dart code. These are mostly enforced by the linter as well.

Dart

  • Max. line length is set to 120 (Dart defaults to 80)
  • Relative imports for all project files below src folder; for example: import 'bloc/wallet_bloc.dart';
  • Trailing commas are added by default; unless it compromises readability

Naming

  • Folder naming is singular for folders below src; for example: src/feature/wallet/widget/...

Testing

Unit tests

  • Test file name follows the convention: {class_name}_test.dart
  • Test description (ideally) follows the convention: should {do something} when {some condition}
  • Tests are grouped* by the method they are testing

** Grouping tests by method is not required, but recommended when testing a specific method.

UI / Golden tests

  • UI Tests are part of the normal test files
  • UI Tests are grouped in Golden Tests

Even though they run headless, UI tests are slower to run. The main goal of these tests are to:

  • Verify correct accessibility behaviour on different configurations (orientation/display scaling/font scaling/theming)
  • Detect unexpected UI changes

As such we aim to keep the UI tests minimal, focusing on testing the most important states for a screen. This can be done by providing a mocked bloc with the state manually configured in the test.

Note that the UI renders slightly differ per platform, causing small diffs (and failing tests) when verifying on a different host platform (e.g. mac vs linux). To circumvent this issue, we opted to only run UI tests on mac hosts for now. Because of this it is vital to only generate new goldens on a mac host. This can be done with flutter test --update-goldens --tags=golden <optional_path_to_single_test_file>.

  • To only verify goldens use flutter test --tags=golden
  • To only verify other tests use flutter test --exclude-tags=golden
Widget Test Template

To be as consistent as possible when it comes to testing widget we provide the following template. This can be used as a starting point when writing widget tests:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';

void main() {
  group('goldens', () {
    testGoldens(
      'light text',
          (tester) async {
        await tester.pumpWidgetWithAppWrapper(
          Text('T'),
        );
        await screenMatchesGolden(tester, 'text/light');
      },
    );
    testGoldens(
      'dark text',
          (tester) async {
        await tester.pumpWidgetWithAppWrapper(
          Text('T'),
          brightness: Brightness.dark,
        );
        await screenMatchesGolden(tester, 'text/dark');
      },
    );
  });

  group('widgets', () {
    testWidgets('widget is visible', (tester) async {
      await tester.pumpWidgetWithAppWrapper(
        Text('T'),
      );

      // Validate that the widget exists
      final widgetFinder = find.text('T');
      expect(widgetFinder, findsOneWidget);
    });
  });
}

Architecture

The project uses the flutter_bloc package to handle state management.

On top of that it follows the BLoC Architecture guide, with a slightly fleshed out domain layer.

This architecture relies on three conceptual layers, namely:

Architecture Overview

UI Layer

Responsible for displaying the application data on screen.

In the above diagram the UI node likely represents a Widget, e.g. in the form of a xyzScreen , that observes the Bloc using one of the flutter_bloc provided Widgets. E.g. BlocBuilder.

When the user interacts with the UI, the UI is responsible for sending a corresponding Event to the associated BLoC. The BLoC then processes this event and emits an updated State to the UI, causing the UI to rebuild and render the new state.

Domain Layer

Encapsulate business logic to make it reusable. UseCases are likely to be used by BLoCs to interact with data, allowing the BLoCs to be concise and keep their focus on converting events into new states.

Naming convention verb in present tense + noun/what (optional) + UseCase e.g. LogoutUserUseCase.

Data Layer

Exposes application data to the rest of the application. This is where we expose the CRUD and network operations. Due to our current requirement of maintaining a 'mock' and a 'core' variant this is the layer where the distinction is made, by injecting either a 'mock' or a 'core' version of the TypedWalletCore based on the MOCK_REPOSITORIES compile time flag. The repositories in term rely on this TypedWalletCore class to perform all the interactions.

Motivation

The reason we opted for this BLoC layered approach with the intermediary domain layer is to optimize for: Re-usability, Testability and Readability.

Re-usable because the usecases in the domain layer are focused on a single task, making them convenient to re-use in multiple blocs when the same data or interaction is required on multiple ( ui) screens.

Testable because with the abstraction to other layers in the form of dependencies of a class, the dependencies can be easily swapped out by mock implementations, allowing us to create small, non-flaky unit tests of all individual components.

Readable because with there is a clear separation of concerns between the layers, the UI is driven by data models (not by state living in UI components) and can thus be easily inspected, there is a single source of truth for the data in the data layer and there is a unidirectional data flow in the ui layer making it easier to reason about the transitions between different states.

Finally, since while we are developing the initial Proof of Concept it is unlikely that we will be working with real datasources, this abstraction allows us to get started now, and in theory quickly migrate to a fully functional app (once the data comes online) by replacing our MockRepositories / MockDataSources with the actual implementations, without touching anything in the Domain or UI Layer.

App Distribution

TestFlight iOS app distribution

The iOS app is distributed through our CI/CD pipeline, but one can follow the steps below in order to deliver a test version of the iOS app to users via TestFlight manually.

Prerequisites

  • Apple ID with access to App Store Connect
  • App-specific password
  • Fastlane Match Passphrase

Credentials and access are available within the team (ask around).

Setup prerequisites (1 time action)

  • Login to appleid.com
  • Create an App-specific password
  • Store the created App-specific password & fastlane username/password as environment variables:
export FASTLANE_USER="{AppleID email address}"
export FASTLANE_PASSWORD="{Fastlane Match Passphrase}"
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="{App-specific password}"

Build & upload IPA

  • Run bundle install from the project root folder
  • Run bundle exec fastlane match appstore --readonly to locally install App Store certificate & provisioning profile (password protected: "Fastlane Match Passphrase")
  • Check latest iOS build number here: App Store Connect - iOS Builds, next build number needs to be {latest_build_numer} + 1
  • Build app with updated build number UL_HOSTNAME=app.example.com bundle exec fastlane ios build app_store:true build:{next_build_number} bundle_id:nl.ictu.edi.wallet.latest app_name:"NL Wallet (latest)" universal_link_base:app.example.com
  • Upload to TestFlight bundle exec fastlane ios deploy bundle_id:nl.ictu.edi.wallet.latest (login with Apple ID + password; app specific password!)