Compare commits
10 Commits
9b14a28d86
...
mobile-app
Author | SHA1 | Date | |
---|---|---|---|
28d47917cf | |||
694884f8c4 | |||
c878c7f327 | |||
8d3b17dcd1 | |||
1781318fdf | |||
2560962684 | |||
7387e285a0 | |||
ff97fb69f7 | |||
c8fa4552bb | |||
ce1c175c62 |
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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!")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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!")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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!")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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!")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
|
After Width: | Height: | Size: 833 B |
After Width: | Height: | Size: 536 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.2 KiB |
BIN
moneymgr_mobile/assets/icon/icon.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
34
moneymgr_mobile/flutter_launcher_icons.yaml
Normal 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"
|
@ -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;
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 345 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 504 B |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 859 B |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 845 B |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 504 B |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 777 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 811 B |
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 967 B |
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.2 KiB |
@ -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>
|
||||||
|
@ -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()],
|
||||||
|
@ -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;
|
||||||
}
|
}
|
48
moneymgr_mobile/lib/routes/login/base_auth_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
91
moneymgr_mobile/lib/routes/login/manual_auth_screen.dart
Normal 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
moneymgr_mobile/lib/services/api/api_client.dart
Normal 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
|
||||||
|
}
|
16
moneymgr_mobile/lib/services/api/api_token.dart
Normal 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);
|
||||||
|
}
|
30
moneymgr_mobile/lib/services/api/auth_api.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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";
|
@ -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");
|
||||||
}
|
}
|
||||||
|
12
moneymgr_mobile/lib/utils/string_utils.dart
Normal 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)),
|
||||||
|
),
|
||||||
|
);
|
3
moneymgr_mobile/lib/utils/time_utils.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
int secondsSinceEpoch(DateTime time) {
|
||||||
|
return time.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
@ -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";
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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>
|
||||||
|