12 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
9b14a28d86 Add settings screen
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-01 22:22:16 +02:00
29fec99b8f Add base skeleton 2025-07-01 20:40:00 +02:00
77 changed files with 1255 additions and 192 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

@ -46,3 +46,4 @@ app.*.map.json
*.g.dart *.g.dart
*.freezed.dart

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

@ -4,4 +4,7 @@
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,8 +2,11 @@ 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/storage/prefs.dart'; import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/services/storage/secure_storage.dart';
import 'package:moneymgr_mobile/utils/provider_observer.dart'; import 'package:moneymgr_mobile/utils/provider_observer.dart';
import 'package:moneymgr_mobile/utils/theme_utils.dart'; import 'package:moneymgr_mobile/utils/theme_utils.dart';
@ -16,10 +19,20 @@ 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);
runApp(ProviderScope( // 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(
ProviderScope(
observers: [AppProviderObserver()], observers: [AppProviderObserver()],
child: const MoneyMgrApp(), child: const MoneyMgrApp(),
)); ),
);
} }
class MoneyMgrApp extends StatelessWidget { class MoneyMgrApp extends StatelessWidget {
@ -27,21 +40,18 @@ class MoneyMgrApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const _EagerInitialization( return const _EagerInitialization(child: _MainApp());
child: _MainApp(),
);
} }
} }
class _EagerInitialization extends ConsumerWidget { class _EagerInitialization extends ConsumerWidget {
const _EagerInitialization({required this.child}); const _EagerInitialization({required this.child});
final Widget child; final Widget child;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final values = [ final values = [ref.watch(prefsProvider), ref.watch(secureStorageProvider)];
ref.watch(prefsProvider),
];
if (values.every((value) => value.hasValue)) { if (values.every((value) => value.hasValue)) {
return child; return child;
@ -58,7 +68,6 @@ class _MainApp extends StatefulHookConsumerWidget {
ConsumerState<_MainApp> createState() => _MainAppState(); ConsumerState<_MainApp> createState() => _MainAppState();
} }
class _MainAppState extends ConsumerState<_MainApp> { class _MainAppState extends ConsumerState<_MainApp> {
@override @override
void initState() { void initState() {
@ -71,7 +80,8 @@ class _MainAppState extends ConsumerState<_MainApp> {
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
final themeMode = ref.watch(currentThemeModeProvider); final themeMode = ref.watch(currentThemeModeProvider);
final (lightTheme, darkTheme) = useMemoized(() => createDualThemeData( final (lightTheme, darkTheme) = useMemoized(
() => createDualThemeData(
seedColor: Colors.blue, seedColor: Colors.blue,
useMaterial3: true, useMaterial3: true,
transformer: (data) => data.copyWith( transformer: (data) => data.copyWith(
@ -79,7 +89,8 @@ class _MainAppState extends ConsumerState<_MainApp> {
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
), ),
)); ),
);
return MaterialApp.router( return MaterialApp.router(
title: 'MoneyMgr', title: 'MoneyMgr',

View File

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

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_gutter/flutter_gutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
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),
],
);
}
}
class _LoginChoice extends StatelessWidget {
const _LoginChoice({
required this.route,
required this.label,
required this.icon,
});
final String route;
final String label;
final IconData icon;
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: () => context.push(route),
style: ButtonStyle(
padding: WidgetStatePropertyAll(
EdgeInsetsGeometry.symmetric(vertical: 20.0, horizontal: 30.0),
),
),
child: Row(
children: [
Icon(icon, size: 25.0),
Gutter(),
Flexible(child: Text(label)),
],
),
);
}
}

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,74 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/providers/settings.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
class SettingsScreen extends ConsumerWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(currentThemeModeProvider);
void onTapThemeMode() =>
showDialog(context: context, builder: (_) => const _ThemeModeDialog());
void onTapLicenses() => context.showAppLicensePage();
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.brightness_6),
title: const Text('Theme mode'),
trailing: Text(themeMode.label),
onTap: onTapThemeMode,
),
const Divider(),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Licenses'),
onTap: onTapLicenses,
),
],
),
);
}
}
/// Select theme dialog
class _ThemeModeDialog extends ConsumerWidget {
const _ThemeModeDialog();
@override
Widget build(BuildContext context, WidgetRef ref) {
void onTapOption(ThemeMode themeMode) {
ref.read(currentThemeModeProvider.notifier).set(themeMode);
Navigator.of(context).pop();
}
return SimpleDialog(
clipBehavior: Clip.antiAlias,
children: [
for (final themeMode in ThemeMode.values)
_ThemeModeDialogOption(
value: themeMode,
onTap: () => onTapOption(themeMode),
),
],
);
}
}
class _ThemeModeDialogOption extends StatelessWidget {
const _ThemeModeDialogOption({required this.value, required this.onTap});
final ThemeMode value;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
return ListTile(onTap: onTap, title: Text(value.label));
}
}

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

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.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/manual_auth_screen.dart';
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'router.g.dart';
/// The router config for the app.
@riverpod
GoRouter router(Ref ref) {
// Local notifier for the current auth state. The purpose of this notifier is
// to provide a [Listenable] to the [GoRouter] exposed by this provider.
final authStateNotifier = ValueNotifier(AuthState.unknown);
ref
..onDispose(authStateNotifier.dispose)
..listen(currentAuthStateProvider, (_, value) {
authStateNotifier.value = value;
});
// This is the only place you need to define your navigation items. The items
// will be propagated automatically to the router and the navigation bar/rail
// of the scaffold.
//
// To configure the authentication state needed to access a particular item,
// see [AuthState] enum.
final navigationItems = [
NavigationItem(
path: '/products',
body: (_) => const Text("product screen"),
icon: Icons.widgets_outlined,
selectedIcon: Icons.widgets,
label: 'Products',
routes: [
GoRoute(
path: ':id',
builder: (_, state) {
final id = int.parse(state.pathParameters['id']!);
return Text("product screen $id");
},
),
],
),
NavigationItem(
path: profilePage,
body: (_) => const Text("Profile"),
icon: Icons.person_outline,
selectedIcon: Icons.person,
label: 'Profile',
),
];
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: navigationItems.first.path,
routes: [
GoRoute(path: homePage, builder: (_, __) => const Scaffold()),
GoRoute(path: authPage, builder: (_, __) => const LoginScreen()),
GoRoute(
path: manualAuthPage,
builder: (_, __) => const ManualAuthScreen(),
),
GoRoute(path: settingsPage, builder: (_, __) => const SettingsScreen()),
// Configuration for the bottom navigation bar routes. The routes themselves
// should be defined in [navigationItems]. Modification to this [ShellRoute]
// config is rarely needed.
ShellRoute(
builder: (_, __, child) => child,
routes: [
for (final (index, item) in navigationItems.indexed)
GoRoute(
path: item.path,
pageBuilder: (context, _) => NoTransitionPage(
child: ScaffoldWithNavigation(
selectedIndex: index,
navigationItems: navigationItems,
child: item.body(context),
),
),
routes: item.routes,
),
],
),
],
refreshListenable: authStateNotifier,
redirect: (_, state) {
// Get the current auth state.
final authState = ref.read(currentAuthStateProvider);
// Check if the current path is allowed for the current auth state. If not,
// redirect to the redirect target of the current auth state.
if (authState.allowedPaths?.contains(state.fullPath) == false ||
authState.forbiddenPaths?.contains(state.fullPath) == true) {
return authState.redirectPath;
}
// If the current path is allowed for the current auth state, don't redirect.
return null;
},
);
ref.onDispose(router.dispose);
return router;
}

View File

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

View File

@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/api/api_token.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'secure_storage.g.dart';
@riverpod
Future<SecureStorage> secureStorage(Ref ref) =>
SecureStorage.getInstance(keys: {'token'});
class SecureStorage {
SecureStorage._(this._flutterSecureStorage, this._cache);
late final FlutterSecureStorage _flutterSecureStorage;
late final Map<String, String> _cache;
static Future<SecureStorage> getInstance({required Set<String> keys}) async {
const flutterSecureStorage = FlutterSecureStorage();
final cache = <String, String>{};
await keys
.map(
(key) => flutterSecureStorage.read(key: key).then((value) {
if (value != null) {
cache[key] = value;
}
}),
)
.wait;
return SecureStorage._(flutterSecureStorage, cache);
}
String? get(String key) => _cache[key];
Future<void> set(String key, String value) {
_cache[key] = value;
return _flutterSecureStorage.write(key: key, value: value);
}
Future<void> remove(String key) {
_cache.remove(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,39 @@
import 'package:flutter/material.dart';
extension BuildContextX on BuildContext {
/// A convenient way to access [ThemeData.colorScheme] of the current context.
///
/// This also prevents confusion with a bunch of other properties of [ThemeData]
/// that are less commonly used.
ColorScheme get colorScheme => Theme.of(this).colorScheme;
/// A convenient way to access [ThemeData.textTheme] of the current context.
///
/// This also prevents confusion with a bunch of other properties of [ThemeData]
/// that are less commonly used.
TextTheme get textTheme => Theme.of(this).textTheme;
/// Shows a floating snack bar with text as its content.
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showTextSnackBar(
String text,
) =>
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(text),
));
void showAppLicensePage() => showLicensePage(
context: this,
useRootNavigator: true,
applicationName: 'MoneyMgr',
applicationLegalese: '(c) Pierre HUBERT 2025 - ${DateTime.now().year}'
);
}
extension ThemeModeX on ThemeMode {
String get label => switch (this) {
ThemeMode.system => 'System',
ThemeMode.light => 'Light',
ThemeMode.dark => 'Dark',
};
}

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
typedef AsyncTask = ({
ValueNotifier<Future<void>?> pending,
AsyncSnapshot<void> snapshot,
bool hasError,
});
/// Creates a hook that provides a [snapshot] of the current asynchronous task passed
/// to [pending] and a [hasError] value.
AsyncTask useAsyncTask() {
final pending = useState<Future<void>?>(null);
final snapshot = useFuture(pending.value);
final hasError =
snapshot.hasError && snapshot.connectionState != ConnectionState.waiting;
return (pending: pending, snapshot: snapshot, hasError: hasError);
}

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

@ -0,0 +1,43 @@
import 'package:flextras/flextras.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gutter/flutter_gutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/hooks.dart';
/// A button that shows a circular progress indicator when the [onPressed] callback
/// is pending.
class AppButton extends HookWidget {
const AppButton({super.key, required this.onPressed, required this.label});
final AsyncCallback? onPressed;
final String label;
@override
Widget build(BuildContext context) {
final (:pending, :snapshot, :hasError) = useAsyncTask();
return FilledButton(
onPressed: onPressed == null ? null : () => pending.value = onPressed!(),
style: ButtonStyle(
backgroundColor: hasError ? WidgetStatePropertyAll(Colors.red) : null,
),
child: SeparatedRow(
separatorBuilder: () => const GutterSmall(),
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (snapshot.connectionState == ConnectionState.waiting)
SizedBox.square(
dimension: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: context.colorScheme.onPrimary,
),
),
Text(label),
],
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// A scaffold that shows navigation bar/rail when the current path is a navigation
/// item.
///
/// When in a navigation item, a [NavigationBar] will be shown if the width of the
/// screen is less than 600dp. Otherwise, a [NavigationRail] will be shown.
class ScaffoldWithNavigation extends StatelessWidget {
const ScaffoldWithNavigation({
super.key,
required this.child,
required this.selectedIndex,
required this.navigationItems,
});
final Widget child;
final int selectedIndex;
final List<NavigationItem> navigationItems;
@override
Widget build(BuildContext context) {
void onDestinationSelected(int index) =>
context.go(navigationItems[index].path);
// Use navigation rail instead of navigation bar when the screen width is
// larger than 600dp.
if (MediaQuery.sizeOf(context).width > 600) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: [
for (final item in navigationItems)
NavigationRailDestination(
icon: Icon(item.icon),
selectedIcon: item.selectedIcon != null
? Icon(item.selectedIcon)
: null,
label: Text(item.label),
)
],
extended: true,
),
Expanded(child: child),
],
),
);
}
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: [
for (final item in navigationItems)
NavigationDestination(
icon: Icon(item.icon),
selectedIcon:
item.selectedIcon != null ? Icon(item.selectedIcon) : null,
label: item.label,
)
],
),
);
}
}
/// An item that represents a navigation destination in a navigation bar/rail.
class NavigationItem {
/// Path in the router.
final String path;
/// Widget to show when navigating to this [path].
final WidgetBuilder body;
/// Icon in the navigation bar.
final IconData icon;
/// Icon in the navigation bar when selected.
final IconData? selectedIcon;
/// Label in the navigation bar.
final String label;
/// The subroutes of the route from this [path].
final List<RouteBase> routes;
NavigationItem({
required this.path,
required this.body,
required this.icon,
this.selectedIcon,
required this.label,
this.routes = const [],
});
}

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

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

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

@ -5,8 +5,12 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import flutter_secure_storage_macos
import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

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:
@ -257,11 +305,35 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flextras:
dependency: "direct main"
description:
name: flextras
sha256: e73b5c86dd9419569d2a48db470059b41b496012513e4e1bdc56ba2c661048d9
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_adaptive_scaffold:
dependency: transitive
description:
name: flutter_adaptive_scaffold
sha256: "5eb1d1d174304a4e67c4bb402ed38cb4a5ebdac95ce54099e91460accb33d295"
url: "https://pub.dev"
source: hosted
version: "0.3.3+1"
flutter_gutter:
dependency: "direct main"
description:
name: flutter_gutter
sha256: "2aa99181796d6f7d2de66da962b71b0feb996ec69b7a1ad2ac1c2119f25b041b"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter_hooks: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
@ -270,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:
@ -294,6 +374,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.1" version: "2.6.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -304,8 +432,16 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
freezed_annotation: freezed_annotation:
dependency: transitive dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
@ -404,18 +540,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.6.7"
json_annotation: json_annotation:
dependency: transitive dependency: "direct main"
description: description:
name: json_annotation name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
url: "https://pub.dev"
source: hosted
version: "6.9.5"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -449,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
@ -504,6 +648,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -552,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:
@ -701,6 +877,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -853,6 +1037,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev"
source: hosted
version: "5.14.0"
xdg_directories: xdg_directories:
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
@ -38,6 +38,9 @@ dependencies:
# Preferences management # Preferences management
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
# Credentials storage
flutter_secure_storage: ^9.2.4
# Splash screen # Splash screen
flutter_native_splash: ^2.4.6 flutter_native_splash: ^2.4.6
@ -51,6 +54,25 @@ dependencies:
# Router # Router
go_router: ^15.2.4 go_router: ^15.2.4
# Flutter extras widgets for columns and rows
flextras: ^1.0.0
flutter_gutter: ^2.2.0
# Help in models building
freezed_annotation: ^3.0.0
# For JSON serialization
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
@ -62,12 +84,23 @@ 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
# Riverpod code generation # Riverpod code generation
riverpod_generator: ^2.6.5 riverpod_generator: ^2.6.5
# Freezed code generation
freezed: ^3.0.6
# JSON serialization
json_serializable: ^6.9.5
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

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

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

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>