/vocdoni-mobile

Decentralized, transparent, verifiable and anonymous voting app

Primary LanguageDartBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Vocdoni Mobile Client

Official implementation of the Vocdoni core features.

Flavors

The app can run in three diferent modes:

  • Development
    • Uses the development blockchain
  • Beta (Android only)
    • Uses the development blockchain
  • Production
    • Uses the production blockchain

Flavors are available on Android. The iOS project uses dev and production depending on the XCode target.

Development

Data Architecture and internal Models

The State Management architecture of the app is built on top of the Eventual package. Eventual allows to track updates on objects and rebuild their corresponding UI accordingly.

Model classes

They can be of the type:

  • Single models
    • AppStateModel, AccountModel, EntityModel, ProcessModel, FeedModel
    • A model can contain references to other models
  • Pools of data
    • Typically, they contain a collection of single models
    • Usually used as global variables
    • Their goal is to track whether the collection changes, but not the individual items
    • They contain all the model instances known to the system
    • Any modification on a model should happen in models obtained from a global pool, since the pool manages persistence

This separation allows for efficient and granular widget tree rebuilds whenever the state is updated. If a single value changes, only the relevant children should rebuild.

Usage

Initialize and read Global Model's data in globals.dart

// entitiesPersistence = EntitiesPersistence();
await Global.entitiesPersistence.readAll();

// ...
// entityPool = EntityPoolModel();
await Globals.entityPool.readFromStorage();  // will import and arrange the persisted data

Consume Models in specific places

Typically, a Pool with all the EntityModel's known to the app and then, individual EntityModel instances when the user selects one.

// Globals.entityPool

// ...

// Widget
@override
Widget build(BuildContext context) {
	// From the pool, we grab the first entity model
	final myEntity = Globals.entityPool.value.first;

	// Consume many values (EventualNotifier) locally
	return EventualBuilder(
    	notifiers: [myEntity.feed, myEntity.processes],  // EventualNotifier<T> values that may change over time
		builder: (context) {
			// rebuilt whenever either of myEntity.feed or myEntity.processes change

			// ...
		)
	);
}

In the example above, updates on specifig Feed items, will not affect the current widget. But as soon as we call myEntity.feed.refresh() on this instance, the Builder will be triggered because of the changes in isLoading, hasError and hasValue.

Extra methods

Certain models implement the ModelRefreshable interface. This ensures that callers can call refresh() to request a refetch of remote data, based on the current model's ID or metadata.

Other models (mainly pools) also implement the ModelPersistable interface, so that readFromStorage() and writeToStorage() can be called.

General

It is important not to mix the models (account, entity, process, feed, app state) with the Persistence classes. Persistence classes map diretly to dvote-protobuf classes, which allow for binary serialization and consistently have a 1:1 mapping.

Models can contain both data which is persisted (entity metadata, process metadata) as well as data that is ephemeral (current participants on a vote, selected account). When readFromStorage is called, data needs to be deserialized and restored properly, often across multiple models.

Internationalization

Translations

  • Add import 'package:vocdoni/lib/i18n.dart'; on your widget file
  • Access the new string with getText(context, "My new string to translate")
  • Parse the new strings with make lang-extract
  • Translate the files on assets/i18n/*.json

The app's strings translation can be found on Weblate.

Translation management

Weblate monitors origin/i18n and pulls from it as new strings are available. After a new translation is added, Weblate compiles the new JSON files in a git remote of its own.

To pull from it, run make init. This will add a weblate git remote besides origin. This way:

  • weblate/i18n has the translated strings to integrate into the app
  • origin/i18n contains the empty strings that Weblate will show to translators

The translation flow is an iterative loop that looks like:

  • Lock the weblate repository
  • git checkout i18n
  • git pull weblate i18n (update our local repo)
  • git push origin i18n (update the GitHub branch)
  • git checkout main (check out the latest code)
  • git merge i18n (integrate the latest translations)
  • git push origin main (update the GitHub branch)
  • git checkout i18n
  • git merge main (integrate the latest code into i18n)
  • make lang-parse (extract the new strings)
  • git add assets/i18n/* (stage the language files for commit)
  • git commit -m "Updated strings"
  • git push origin i18n (push the new strings for Weblate)
  • git checkout main
  • Unlock the Weblate repository

Important:

  • Make sure to use the right key prefixes:
    • "action.createIdentity" instead of "main.createIdentity"
  • In the keys, use no symbols beyond the dot separator
  • Use placeholders for data replacement instead of concatenating it later
    • "question.doYouWantToRemoveName" => "Do you want to remove {{NAME}}?"

Dependencies

The project makes use of the DVote Flutter plugin. Please, see the repository for more details.

Integration

Deep linking

The app accepts Deep Links from the following domains:

  • app.vocdoni.net
  • app.dev.vocdoni.net
  • vocdoni.page.link
  • vocdonidev.page.link

To enable them:

  • Place linking/assetlink.json on https://app.vocdoni.net/.well-known/assetlinks.json
    • Also place linking/assetlink.json on https://app.dev.vocdoni.net/.well-known/assetlinks.json
  • Place linking/apple-app-site-association on https://app.vocdoni.net/.well-known/apple-app-site-association

Supported Paths and Parameters

  • app.vocdoni.net and app.dev.vocdoni.net
    • https://app.vocdoni.net/entities/#/<entity-id>
    • https://app.vocdoni.net/entities/#/<process-id>
    • https://app.vocdoni.net/news/#/<process-id>
    • https://app.vocdoni.net/validation/#/<entity-id>/<validation-token>

The same applies to app.dev.vocdoni.net

  • vocdoni.page.link and vocdonidev.page.link
    • They wrap dynamic links
    • The link query string parameter is extracted, which should contain a link like the ones above from app.vocdoni.net and app.dev.vocdoni.net

Supported Schemas

The app also accepts URI's using the vocdoni: schema, with the same paths and parameters as above:

  • vocdoni://vocdoni.app/entities/#/<entity-id>
  • vocdoni://vocdoni.app/processes/#/<entity-id>/<process-id>
  • vocdoni://vocdoni.app/posts/#/<entity-id>/<idx>
  • vocdoni://vocdoni.app/validation/#/<entity-id>/<validation-token>

Show an entity

On developoment, you can test it by running make launch-ios-org or make launch-android-org

Push notifications

The data field of incoming push notifications is expected to contain three keys:

{
   notification: { ... },
   data: {
      uri: "https://vocdoni.link/processes/0x1234.../0x2345...",
      event: "new-process", // enum, see below
      message: "..." // same as notification > body
   }
}
  • The uri field will be used the same as a deep link and will determine where the app navigates to.
  • The event can be one of:
    • entity-updated: The metadata of the entity has changed (but not the process list or the news feed)
    • new-post: A new post has been added to the Feed
    • new-process: A process has been created
    • process-ended: The end block has been reached
  • The message is a copy of the relevant text contained within notification (not always present)

Permissions

In order to drop unused permissions, edit ios/Podfile and make sure to uncomment the permissions that are not needed after target.build_configurations.each.

Troubleshooting

  • The r_scan plugin breaks the build on iOS and/or is rejected by Google Play
    • Edit pubspec.yaml and comment/uncomment the dependencies accordingly
  • Can't compile iOS because App.framework is built for another architecture
    • Run rm -Rf ios/Flutter/App.framework and try again