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.
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.
After setting up your environment, launch an Android emulator or the iOS simulator and execute:
flutter run
orfvm flutter run
when using Flutter Version Manager (FVM)
Note that when using FVM, all Flutter commands below should be prefixed with fvm
.
The easiest way to build the app locally is to use fastlane.
To install use: bundle install
.
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.
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:
- Get the
nl-wallet-android-local-signing-key
secrets from the secrets repository. - Move
local-keystore.jks
file into thewallet_app/android/keystore
folder. - Move the
key.properties
file into thewallet_app/android
folder. - That's it! Building release builds, e.g. with
bundle exec fastlane android build
should now work.
We are currently maintaining two 'flavours' of the app, a mock and an core version.
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
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.
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.
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.
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.
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 |
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 |
Scenario | Content | Deeplink |
---|---|---|
Rental Agreement | {"id":"RENTAL_AGREEMENT","type":"sign"} | Sign rental agreement |
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 |
This section specifies some of the conventions we use to format our .dart code. These are mostly enforced by the linter as well.
- 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
- Folder naming is
singular
for folders belowsrc
; for example:src/feature/wallet/widget/...
- 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 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
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);
});
});
}
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:
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.
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.
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.
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.
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.
- Apple ID with access to App Store Connect
- App-specific password
- Fastlane Match Passphrase
Credentials and access are available within the team (ask around).
- 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}"
- 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!)