10 Commits

Author SHA1 Message Date
28d47917cf Refactor repo to fix package name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-03 23:09:36 +02:00
694884f8c4 Refacto source code following package name change
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-03 23:04:47 +02:00
c878c7f327 Set application icons
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-03 23:00:24 +02:00
8d3b17dcd1 Merge remote-tracking branch 'origin/main' into mobile-app
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-03 22:32:55 +02:00
1781318fdf Fix bad backend URL on generated Qr Code for tokens authentication
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-03 22:00:20 +02:00
2560962684 Fix cargo clippy issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-03 08:38:56 +02:00
7387e285a0 Fix missing redirect after login
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-02 22:48:53 +02:00
ff97fb69f7 Performed first authentication
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-02 22:14:52 +02:00
c8fa4552bb Add manual auth screen
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-02 19:50:25 +02:00
ce1c175c62 Adapt base login page
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-01 22:45:20 +02:00
68 changed files with 576 additions and 288 deletions

View File

@ -30,7 +30,7 @@ pub async fn create_bucket_if_required() -> anyhow::Result<()> {
log::warn!("The bucket does not seem to exists, trying to create it!") log::warn!("The bucket does not seem to exists, trying to create it!")
} }
Err(e) => { Err(e) => {
log::error!("Got unexpected error when querying bucket info: {}", e); log::error!("Got unexpected error when querying bucket info: {e}");
return Err(BucketServiceError::FailedFetchBucketInfo.into()); return Err(BucketServiceError::FailedFetchBucketInfo.into());
} }
} }

View File

@ -50,7 +50,7 @@ impl FromRequest for AccountInPath {
Self::load_account_from_path(&auth, account_id) Self::load_account_from_path(&auth, account_id)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to extract account ID from URL! {}", e); log::error!("Failed to extract account ID from URL! {e}");
actix_web::error::ErrorNotFound("Could not fetch account information!") actix_web::error::ErrorNotFound("Could not fetch account information!")
}) })
}) })

View File

@ -165,13 +165,13 @@ impl FromRequest for AuthExtractor {
// Update last use (if needed) // Update last use (if needed)
if token.shall_update_time_used() { if token.shall_update_time_used() {
if let Err(e) = tokens_service::update_time_used(&token).await { if let Err(e) = tokens_service::update_time_used(&token).await {
log::error!("Failed to refresh last usage of token! {}", e); log::error!("Failed to refresh last usage of token! {e}");
} }
} }
// Handle tokens expiration // Handle tokens expiration
if token.is_expired() { if token.is_expired() {
log::error!("Attempted to use expired token! {:?}", token); log::error!("Attempted to use expired token! {token:?}");
return Err(actix_web::error::ErrorBadRequest("Token has expired!")); return Err(actix_web::error::ErrorBadRequest("Token has expired!"));
} }

View File

@ -47,7 +47,7 @@ impl FromRequest for FileIdExtractor {
Self::load_file_from_path(&auth, file_id) Self::load_file_from_path(&auth, file_id)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to extract file ID from URL! {}", e); log::error!("Failed to extract file ID from URL! {e}");
actix_web::error::ErrorNotFound("Could not fetch file information!") actix_web::error::ErrorNotFound("Could not fetch file information!")
}) })
}) })

View File

@ -50,7 +50,7 @@ impl FromRequest for InboxEntryInPath {
Self::load_inbox_entry_from_path(&auth, entry_id) Self::load_inbox_entry_from_path(&auth, entry_id)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to extract inbox entry ID from URL! {}", e); log::error!("Failed to extract inbox entry ID from URL! {e}");
actix_web::error::ErrorNotFound("Could not fetch inbox entry information!") actix_web::error::ErrorNotFound("Could not fetch inbox entry information!")
}) })
}) })

View File

@ -57,7 +57,7 @@ impl FromRequest for MovementInPath {
Self::load_movement_from_path(&auth, account_id) Self::load_movement_from_path(&auth, account_id)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("Failed to extract movement ID from URL! {}", e); log::error!("Failed to extract movement ID from URL! {e}");
actix_web::error::ErrorNotFound("Could not fetch movement information!") actix_web::error::ErrorNotFound("Could not fetch movement information!")
}) })
}) })

View File

@ -6,7 +6,7 @@ plugins {
} }
android { android {
namespace = "com.example.moneymgr_mobile" namespace = "org.communiquons.moneymgr"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
// ndkVersion = flutter.ndkVersion // ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973" ndkVersion = "27.0.12077973"
@ -22,7 +22,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.moneymgr_mobile" applicationId = "org.communiquons.moneymgr"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@ -3,5 +3,8 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
<!-- In debug mode, unsecure traffic is permitted -->
<application android:usesCleartextTraffic="true" />
</manifest> </manifest>

View File

@ -1,8 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="moneymgr_mobile" android:label="MoneyMgr"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/launcher_icon">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -42,4 +42,7 @@
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
</queries> </queries>
<!-- Communication with backend -->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View File

@ -1,4 +1,4 @@
package com.example.moneymgr_mobile package org.communiquons.moneymgr
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,34 @@
# flutter pub run flutter_launcher_icons
flutter_launcher_icons:
image_path: "assets/icon/icon.png"
android: "launcher_icon"
# image_path_android: "assets/icon/icon.png"
min_sdk_android: 21 # android min sdk min:16, default 21
# adaptive_icon_background: "assets/icon/background.png"
# adaptive_icon_foreground: "assets/icon/foreground.png"
# adaptive_icon_foreground_inset: 16
# adaptive_icon_monochrome: "assets/icon/monochrome.png"
ios: true
# image_path_ios: "assets/icon/icon.png"
remove_alpha_ios: true
# image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
# image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
# desaturate_tinted_to_grayscale_ios: true
# background_color_ios: "#ffffff"
# web:
# generate: true
# image_path: "path/to/image.png"
# background_color: "#hexcode"
# theme_color: "#hexcode"
# windows:
# generate: true
# image_path: "path/to/image.png"
# icon_size: 48 # min:48, max:256, default: 48
# macos:
# generate: true
# image_path: "path/to/image.png"

View File

@ -368,7 +368,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -547,7 +547,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -1,122 +1 @@
{ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>moneymgr_mobile</string> <string>moneymgr</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/providers/settings.dart'; import 'package:moneymgr_mobile/providers/settings.dart';
import 'package:moneymgr_mobile/services/router/router.dart'; import 'package:moneymgr_mobile/services/router/router.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart';
@ -18,6 +19,14 @@ Future<void> main() async {
// app is inserted to the widget tree. // app is inserted to the widget tree.
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
// Configure logger
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen(
(record) =>
// ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}'),
);
runApp( runApp(
ProviderScope( ProviderScope(
observers: [AppProviderObserver()], observers: [AppProviderObserver()],

View File

@ -1,38 +1,38 @@
import 'package:moneymgr_mobile/routes/login/login_model.dart'; import 'package:moneymgr_mobile/services/api/api_client.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart'; import 'package:moneymgr_mobile/services/api/api_token.dart';
import 'package:moneymgr_mobile/services/api/auth_api.dart';
import 'package:moneymgr_mobile/services/storage/secure_storage.dart'; import 'package:moneymgr_mobile/services/storage/secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../services/router/routes_list.dart';
part 'auth_state.g.dart'; part 'auth_state.g.dart';
/// The current authentication state of the app. /// The current authentication state of the app.
/// ///
/// This notifier is responsible for saving/removing the token and profile info /// This notifier is responsible for saving/removing the token and profile info
/// to the storage through the [login] and [logout] methods. /// to the storage through the [setAuthToken] and [logout] methods.
@riverpod @riverpod
class CurrentAuthState extends _$CurrentAuthState { class CurrentAuthState extends _$CurrentAuthState {
@override @override
AuthState build() { AuthState build() {
final secureStorage = ref.watch(secureStorageProvider).requireValue; final secureStorage = ref.watch(secureStorageProvider).requireValue;
final token = secureStorage.get('token'); final token = secureStorage.token();
return token != null ? AuthState.authenticated : AuthState.unauthenticated; return token != null ? AuthState.authenticated : AuthState.unauthenticated;
} }
/// Attempts to log in with [data] and saves the token and profile info to storage. /// Attempts to authenticate with [token] and saves the token and profile info to storage.
/// Will invalidate the state if success. /// Will invalidate the state if success and throw an exception in case of failure
Future<void> login(LoginModel data) async { Future<void> setAuthToken(ApiToken token) async {
// TODO : perform login // Attempt to use provided token
/*final secureStorage = ref.read(secureStorageProvider).requireValue; await ApiClient(token: token).authInfo();
final token = await ref.read(apiServiceProvider).login(data);
// Save the new [token] and [profile] to secure storage. final secureStorage = ref.read(secureStorageProvider).requireValue;
secureStorage.set('token', token); await secureStorage.setToken(token);
ref ref
// Invalidate the state so the auth state will be updated to authenticated. // Invalidate the state so the auth state will be updated to authenticated.
..invalidateSelf() .invalidateSelf();
// Invalidate the token provider so the API service will use the new token.
..invalidate(tokenProvider);*/
} }
/// Logs out, deletes the saved token and profile info from storage, and invalidates /// Logs out, deletes the saved token and profile info from storage, and invalidates
@ -53,17 +53,24 @@ class CurrentAuthState extends _$CurrentAuthState {
} }
} }
/// The possible authentication states of the app. /// The possible authentication states of the app.
enum AuthState { enum AuthState {
unknown(redirectPath: homePage, allowedPaths: [homePage]), unknown(redirectPath: homePage, allowedPaths: [homePage]),
unauthenticated( unauthenticated(
redirectPath: authPage, redirectPath: authPage,
allowedPaths: [authPage, settingsPage], allowedPaths: [authPage, manualAuthPage, settingsPage],
), ),
authenticated(redirectPath: homePage, allowedPaths: null); authenticated(
redirectPath: homePage,
allowedPaths: null,
forbiddenPaths: [authPage, manualAuthPage],
);
const AuthState({required this.redirectPath, required this.allowedPaths}); const AuthState({
required this.redirectPath,
required this.allowedPaths,
this.forbiddenPaths,
});
/// The target path to redirect when the current route is not allowed in this /// The target path to redirect when the current route is not allowed in this
/// auth state. /// auth state.
@ -72,4 +79,8 @@ enum AuthState {
/// List of paths allowed when the app is in this auth state. May be set to null if there is no /// List of paths allowed when the app is in this auth state. May be set to null if there is no
/// restriction applicable /// restriction applicable
final List<String>? allowedPaths; final List<String>? allowedPaths;
/// List of paths not allowed when the app is in this auth state. May be set to null if there is no
/// restriction applicable
final List<String>? forbiddenPaths;
} }

View File

@ -0,0 +1,48 @@
import 'package:flextras/flextras.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gutter/flutter_gutter.dart';
import 'package:go_router/go_router.dart';
import '../../services/router/routes_list.dart';
class BaseAuthPage extends StatelessWidget {
final List<Widget> children;
final String? title;
final bool? showSettings;
const BaseAuthPage({
super.key,
required this.children,
this.title,
this.showSettings,
});
@override
Widget build(BuildContext context) {
void onSettingsPressed() => context.push(settingsPage);
return Scaffold(
appBar: AppBar(
title: Text(title ?? 'MoneyMgr'),
actions: [
// Settings button
showSettings != false
? IconButton(
onPressed: onSettingsPressed,
icon: const Icon(Icons.settings),
)
: Container(),
],
),
body: SingleChildScrollView(
child: SeparatedColumn(
padding: EdgeInsets.all(context.gutter),
separatorBuilder: () => const Gutter(),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
);
}
}

View File

@ -1,16 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_model.freezed.dart';
part 'login_model.g.dart';
@freezed
abstract class LoginModel with _$LoginModel {
const factory LoginModel({
// TODO : replace
required String username,
required String password,
}) = _LoginModel;
factory LoginModel.fromJson(Map<String, dynamic> json) =>
_$LoginModelFromJson(json);
}

View File

@ -1,84 +1,64 @@
import 'package:flextras/flextras.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:flutter_gutter/flutter_gutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/routes/login/login_model.dart'; import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart'; import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/widgets/app_button.dart';
import '../../../services/auth_state.dart';
import '../../../utils/extensions.dart';
class LoginScreen extends HookConsumerWidget { class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState(false); return BaseAuthPage(
children: [
Gutter(scaleFactor: 3),
Text(
"This application requires a token from MoneyMgr to be used.\n\nPlease create a token on your Money Manager instance and make sure to click on \"For mobile app\" button. You can then enter here generated credentials.",
textAlign: TextAlign.justify,
),
Expanded(child: Container()),
_LoginChoice(
route: manualAuthPage,
icon: Icons.edit_document,
label: "Enter manually authentication information",
),
_LoginChoice(
route: manualAuthPage,
icon: Icons.qr_code_2,
label: "Scan authentication Qr Code",
),
Gutter(scaleFactor: 3),
],
);
}
}
final usernameController = useTextEditingController(); class _LoginChoice extends StatelessWidget {
final passwordController = useTextEditingController(); const _LoginChoice({
required this.route,
required this.label,
required this.icon,
});
void onSettingsPressed() => context.push(settingsPage); final String route;
final String label;
final IconData icon;
Future<void> onLoginPressed() async { @override
try { Widget build(BuildContext context) {
await ref.read(currentAuthStateProvider.notifier).login(LoginModel( return FilledButton(
username: usernameController.text, onPressed: () => context.push(route),
password: passwordController.text, style: ButtonStyle(
)); padding: WidgetStatePropertyAll(
} on Exception catch (e) { EdgeInsetsGeometry.symmetric(vertical: 20.0, horizontal: 30.0),
if (!context.mounted) return; ),
context.showTextSnackBar(e.toString());
}
}
return Scaffold(
appBar: AppBar(
title: const Text('MoneyMgr'),
actions: [
IconButton(
onPressed: onSettingsPressed,
icon: const Icon(Icons.settings),
),
],
), ),
body: SeparatedColumn( child: Row(
padding: EdgeInsets.all(context.gutter),
separatorBuilder: () => const Gutter(),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextField( Icon(icon, size: 25.0),
controller: usernameController, Gutter(),
decoration: const InputDecoration(labelText: 'Username'), Flexible(child: Text(label)),
textInputAction: TextInputAction.next,
),
TextField(
controller: passwordController,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () =>
isPasswordVisible.value = !isPasswordVisible.value,
),
),
obscureText: !isPasswordVisible.value,
keyboardType: TextInputType.visiblePassword,
textInputAction: TextInputAction.done,
),
const SizedBox.shrink(),
AppButton(
onPressed: onLoginPressed,
label: 'Login',
),
], ],
), ),
); );

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_gutter/flutter_gutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/providers/auth_state.dart';
import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
import 'package:moneymgr_mobile/services/api/api_token.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/widgets/app_button.dart';
class ManualAuthScreen extends HookConsumerWidget {
const ManualAuthScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
var apiUrlController = useTextEditingController();
var tokenIdController = useTextEditingController();
var tokenValueController = useTextEditingController();
Future<void> onSubmit() async {
try {
await ref
.read(currentAuthStateProvider.notifier)
.setAuthToken(
ApiToken(
apiUrl: apiUrlController.text,
tokenId: int.tryParse(tokenIdController.text) ?? 1,
tokenValue: tokenValueController.text,
),
);
if (context.mounted) {
if (context.canPop()) context.pop();
context.replace(profilePage);
}
} catch (e, s) {
Logger.root.severe("Failed to authenticate user! $e $s");
if (context.mounted) {
context.showTextSnackBar("Failed to authenticate user! $e");
}
}
}
return BaseAuthPage(
title: "Manual authentication",
showSettings: false,
children: [
Gutter(scaleFactor: 3),
Text(
"On this screen you can manually enter authentication information.",
),
Gutter(),
TextField(
controller: apiUrlController,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'API URL',
helperText: "Format: http://moneymgr.corp.com/api",
),
textInputAction: TextInputAction.next,
),
Gutter(),
TextField(
controller: tokenIdController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Token ID',
helperText: "The ID of the token",
),
textInputAction: TextInputAction.next,
),
Gutter(),
TextField(
controller: tokenValueController,
keyboardType: TextInputType.text,
decoration: const InputDecoration(
labelText: 'Token value',
helperText: "The value of the token itself",
),
textInputAction: TextInputAction.done,
),
Gutter(),
AppButton(onPressed: onSubmit, label: "Submit"),
Gutter(scaleFactor: 3),
],
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dio/dio.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/api/api_token.dart';
import 'package:moneymgr_mobile/utils/string_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'api_client.g.dart';
/// API token header
const apiTokenHeader = "X-Auth-Token";
/// Client API
class ApiClient {
final ApiToken token;
final Dio client;
ApiClient({required this.token})
: client = Dio(BaseOptions(baseUrl: token.apiUrl));
/// Get Dio instance
Future<Response<T>> execute<T>(String uri, {String method = "GET"}) async {
return client.request(
uri,
options: Options(
method: method,
headers: {apiTokenHeader: _genJWT(method, uri)},
),
);
}
/// Generate authentication JWT
String _genJWT(String method, String uri) {
final jwt = JWT(
{"nonce": getRandomString(15), "met": method, "uri": uri},
header: {"kid": token.tokenId.toString()},
);
return jwt.sign(
SecretKey(token.tokenValue),
algorithm: JWTAlgorithm.HS256,
expiresIn: Duration(minutes: 15),
);
}
}
/// An API service that handles authentication and exposes an [ApiClient].
///
/// Every API call coming from UI should watch/read this provider instead of
/// instantiating the [ApiClient] itself. When being watched, it will force any
/// data provider (provider that fetches data) to refetch when the
/// authentication state changes.
///
/// The API client is kept alive to follow dio's recommendation to use the same
/// client instance for the entire app.
@riverpod
ApiClient apiService(Ref ref) {
/*final token = ref.watch(currentAuthStateProvider);
final ApiClient client;
const mock = bool.fromEnvironment('MOCK_API', defaultValue: false);
client = switch (mock) {
true =>
token != null ? MockedApiClient.withToken(token) : MockedApiClient(),
false => token != null ? ApiClient.withToken(token) : ApiClient(),
};
ref.keepAlive();
return client;*/
throw Exception("TODO"); // TODO
}

View File

@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_token.freezed.dart';
part 'api_token.g.dart';
@freezed
abstract class ApiToken with _$ApiToken {
const factory ApiToken({
required String apiUrl,
required int tokenId,
required String tokenValue,
}) = _ApiToken;
factory ApiToken.fromJson(Map<String, dynamic> json) =>
_$ApiTokenFromJson(json);
}

View File

@ -0,0 +1,30 @@
// ignore_for_file: non_constant_identifier_names
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moneymgr_mobile/services/api/api_client.dart';
part 'auth_api.g.dart';
part 'auth_api.freezed.dart';
@freezed
abstract class AuthInfo with _$AuthInfo {
const factory AuthInfo({
required int id,
required String mail,
required String name,
required int time_create,
required int time_update,
}) = _AuthInfo;
factory AuthInfo.fromJson(Map<String, dynamic> json) =>
_$AuthInfoFromJson(json);
}
/// Auth API
extension AuthApi on ApiClient {
/// Get authentication information
Future<AuthInfo> authInfo() async {
final response = await execute("/auth/info", method: "GET");
return AuthInfo.fromJson(response.data);
}
}

View File

@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/providers/auth_state.dart';
import 'package:moneymgr_mobile/routes/login/login_screen.dart'; import 'package:moneymgr_mobile/routes/login/login_screen.dart';
import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart'; import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
import 'package:moneymgr_mobile/services/auth_state.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart'; import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart'; import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'router.g.dart'; part 'router.g.dart';
/// The router config for the app. /// The router config for the app.
@ -47,7 +47,7 @@ GoRouter router(Ref ref) {
], ],
), ),
NavigationItem( NavigationItem(
path: '/profile', path: profilePage,
body: (_) => const Text("Profile"), body: (_) => const Text("Profile"),
icon: Icons.person_outline, icon: Icons.person_outline,
selectedIcon: Icons.person, selectedIcon: Icons.person,
@ -62,9 +62,10 @@ GoRouter router(Ref ref) {
GoRoute(path: homePage, builder: (_, __) => const Scaffold()), GoRoute(path: homePage, builder: (_, __) => const Scaffold()),
GoRoute(path: authPage, builder: (_, __) => const LoginScreen()), GoRoute(path: authPage, builder: (_, __) => const LoginScreen()),
GoRoute( GoRoute(
path: settingsPage, path: manualAuthPage,
builder: (_, __) => const SettingsScreen(), builder: (_, __) => const ManualAuthScreen(),
), ),
GoRoute(path: settingsPage, builder: (_, __) => const SettingsScreen()),
// Configuration for the bottom navigation bar routes. The routes themselves // Configuration for the bottom navigation bar routes. The routes themselves
// should be defined in [navigationItems]. Modification to this [ShellRoute] // should be defined in [navigationItems]. Modification to this [ShellRoute]
@ -94,7 +95,8 @@ GoRouter router(Ref ref) {
// Check if the current path is allowed for the current auth state. If not, // Check if the current path is allowed for the current auth state. If not,
// redirect to the redirect target of the current auth state. // redirect to the redirect target of the current auth state.
if (authState.allowedPaths?.contains(state.fullPath) == false) { if (authState.allowedPaths?.contains(state.fullPath) == false ||
authState.forbiddenPaths?.contains(state.fullPath) == true) {
return authState.redirectPath; return authState.redirectPath;
} }

View File

@ -1,7 +1,14 @@
/// Base home page
const homePage = "/"; const homePage = "/";
/// Authentication path /// Authentication path
const authPage = "/login"; const authPage = "/login";
/// Manual authentication
const manualAuthPage = "/login/manual";
/// Settings path /// Settings path
const settingsPage = "/settings"; const settingsPage = "/settings";
// Profile path
const profilePage = "/profile";

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/api/api_token.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'secure_storage.g.dart'; part 'secure_storage.g.dart';
@ -19,11 +22,13 @@ class SecureStorage {
const flutterSecureStorage = FlutterSecureStorage(); const flutterSecureStorage = FlutterSecureStorage();
final cache = <String, String>{}; final cache = <String, String>{};
await keys await keys
.map((key) => flutterSecureStorage.read(key: key).then((value) { .map(
if (value != null) { (key) => flutterSecureStorage.read(key: key).then((value) {
cache[key] = value; if (value != null) {
} cache[key] = value;
})) }
}),
)
.wait; .wait;
return SecureStorage._(flutterSecureStorage, cache); return SecureStorage._(flutterSecureStorage, cache);
} }
@ -39,4 +44,19 @@ class SecureStorage {
_cache.remove(key); _cache.remove(key);
return _flutterSecureStorage.delete(key: key); return _flutterSecureStorage.delete(key: key);
} }
/// Get auth token
ApiToken? token() {
final tokenStr = get("token");
if (tokenStr != null) {
return ApiToken.fromJson(jsonDecode(tokenStr));
}
}
/// Set auth token
Future<void> setToken(ApiToken token) =>
set("token", jsonEncode(token.toJson()));
/// Remove auth token
Future<void> removeToken() => remove("token");
} }

View File

@ -0,0 +1,12 @@
import 'dart:math';
const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final _rnd = Random();
/// Generate random string
String getRandomString(int length) => String.fromCharCodes(
Iterable.generate(
length,
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
),
);

View File

@ -0,0 +1,3 @@
int secondsSinceEpoch(DateTime time) {
return time.millisecondsSinceEpoch ~/ 1000;
}

View File

@ -6,25 +6,23 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:moneymgr_mobile/utils/extensions.dart'; import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/hooks.dart'; import 'package:moneymgr_mobile/utils/hooks.dart';
/// A button that shows a circular progress indicator when the [onPressed] callback /// A button that shows a circular progress indicator when the [onPressed] callback
/// is pending. /// is pending.
class AppButton extends HookWidget { class AppButton extends HookWidget {
const AppButton({ const AppButton({super.key, required this.onPressed, required this.label});
super.key,
required this.onPressed,
required this.label,
});
final AsyncCallback? onPressed; final AsyncCallback? onPressed;
final String label; final String label;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final (:pending, :snapshot, hasError: _) = useAsyncTask(); final (:pending, :snapshot, :hasError) = useAsyncTask();
return FilledButton( return FilledButton(
onPressed: onPressed == null ? null : () => pending.value = onPressed!(), onPressed: onPressed == null ? null : () => pending.value = onPressed!(),
style: ButtonStyle(
backgroundColor: hasError ? WidgetStatePropertyAll(Colors.red) : null,
),
child: SeparatedRow( child: SeparatedRow(
separatorBuilder: () => const GutterSmall(), separatorBuilder: () => const GutterSmall(),
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change # The name of the executable created for the application. Change this to change
# the on-disk name of your application. # the on-disk name of your application.
set(BINARY_NAME "moneymgr_mobile") set(BINARY_NAME "moneymgr")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.moneymgr_mobile") set(APPLICATION_ID "org.communiquons.moneymgr")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View File

@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) { if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar)); gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "moneymgr_mobile"); gtk_header_bar_set_title(header_bar, "MoneyMgr");
gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else { } else {
gtk_window_set_title(window, "moneymgr_mobile"); gtk_window_set_title(window, "MoneyMgr");
} }
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);

View File

@ -385,7 +385,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile";
@ -399,7 +399,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile";
@ -413,7 +413,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/moneymgr_mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/moneymgr_mobile";

View File

@ -5,10 +5,10 @@
// 'flutter create' template. // 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window. // The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = moneymgr_mobile PRODUCT_NAME = MoneyMgr
// The application's bundle identifier // The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.moneymgrMobile PRODUCT_BUNDLE_IDENTIFIER = org.communiquons.moneymgrMobile
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2025 Pierre HUBERT All rights reserved.

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "82.0.0" version: "82.0.0"
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -145,6 +153,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.4" version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -217,6 +233,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+7.4.5" version: "1.0.0+7.4.5"
dart_jsonwebtoken:
dependency: "direct main"
description:
name: dart_jsonwebtoken
sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -225,6 +249,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
dio:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
ed25519_edwards:
dependency: transitive
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -294,6 +342,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.21.2" version: "0.21.2"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -537,7 +593,7 @@ packages:
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
logging: logging:
dependency: transitive dependency: "direct main"
description: description:
name: logging name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
@ -664,6 +720,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
pool: pool:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,5 +1,5 @@
name: moneymgr_mobile name: moneymgr_mobile
description: "A new Flutter project." description: "Mobile application for MoneyMgr"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@ -61,8 +61,18 @@ dependencies:
# Help in models building # Help in models building
freezed_annotation: ^3.0.0 freezed_annotation: ^3.0.0
# For JSON serialization
json_annotation: ^4.9.0 json_annotation: ^4.9.0
# Logger
logging: ^1.3.0
# API authentication
dart_jsonwebtoken: ^3.2.0
# API requests
dio: ^5.8.0+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -74,6 +84,9 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
# Manage app icon
flutter_launcher_icons: ^0.14.4
# Generate source code # Generate source code
build_runner: ^2.5.4 build_runner: ^2.5.4

View File

@ -1,6 +1,6 @@
{ {
"name": "moneymgr_mobile", "name": "MoneyMgr",
"short_name": "moneymgr_mobile", "short_name": "MoneyMgr",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",

View File

@ -4,7 +4,7 @@ project(moneymgr_mobile LANGUAGES CXX)
# The name of the executable created for the application. Change this to change # The name of the executable created for the application. Change this to change
# the on-disk name of your application. # the on-disk name of your application.
set(BINARY_NAME "moneymgr_mobile") set(BINARY_NAME "moneymgr")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View File

@ -89,13 +89,13 @@ BEGIN
BEGIN BEGIN
BLOCK "040904e4" BLOCK "040904e4"
BEGIN BEGIN
VALUE "CompanyName", "com.example" "\0" VALUE "CompanyName", "org.communiquons" "\0"
VALUE "FileDescription", "moneymgr_mobile" "\0" VALUE "FileDescription", "moneymgr" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "moneymgr_mobile" "\0" VALUE "InternalName", "moneymgr" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright (C) 2025 Pierre HUBERT. All rights reserved." "\0"
VALUE "OriginalFilename", "moneymgr_mobile.exe" "\0" VALUE "OriginalFilename", "moneymgr.exe" "\0"
VALUE "ProductName", "moneymgr_mobile" "\0" VALUE "ProductName", "moneymgr" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"
END END
END END

View File

@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project); FlutterWindow window(project);
Win32Window::Point origin(10, 10); Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); Win32Window::Size size(1280, 720);
if (!window.Create(L"moneymgr_mobile", origin, size)) { if (!window.Create(L"MoneyMgr", origin, size)) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
window.SetQuitOnClose(true); window.SetQuitOnClose(true);

View File

@ -31,12 +31,21 @@ export class APIClient {
return URL; return URL;
} }
/**
* Get the full URL at which the backend can be contacted
*/
static ActualBackendURL(): string {
const backendURL = this.backendURL();
if (backendURL.startsWith("/")) return `${location.origin}${backendURL}`;
else return backendURL;
}
/** /**
* Check out whether the backend is accessed through * Check out whether the backend is accessed through
* HTTPS or not * HTTPS or not
*/ */
static IsBackendSecure(): boolean { static IsBackendSecure(): boolean {
return this.backendURL().startsWith("https"); return this.ActualBackendURL().startsWith("https");
} }
/** /**

View File

@ -268,7 +268,7 @@ function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement {
<div style={{ padding: "15px", backgroundColor: "white" }}> <div style={{ padding: "15px", backgroundColor: "white" }}>
<QRCodeCanvas <QRCodeCanvas
value={`moneymgr://api=${encodeURIComponent( value={`moneymgr://api=${encodeURIComponent(
APIClient.backendURL() APIClient.ActualBackendURL()
)}&id=${p.token.id}&secret=${p.token.token}`} )}&id=${p.token.id}&secret=${p.token.token}`}
/> />
</div> </div>