Compare commits

..

25 Commits

Author SHA1 Message Date
Van Leemput Dayron
e5b2be5245 Retry
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 14m25s
2025-12-09 16:26:23 +01:00
Van Leemput Dayron
0fb1634a91 fix: Configure Gradle JVM memory with GRADLE_OPTS and update workflow comments.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 14m55s
2025-12-09 16:02:56 +01:00
Van Leemput Dayron
8634edc916 feat: Enforce portrait orientation across platforms and switch Android Fastlane deployment from APK to AAB.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 13m51s
2025-12-09 15:29:00 +01:00
Van Leemput Dayron
1211569078 ci: add step to create .env file from secrets in Android deploy workflow
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 12m5s
2025-12-09 15:04:33 +01:00
Van Leemput Dayron
26b970982c chore: ensure Android license acceptance step always succeeds in CI workflow.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 11m0s
2025-12-09 14:51:22 +01:00
Van Leemput Dayron
e77393dd13 ci: update deploy-android workflow to trigger only on the release branch
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 1m23s
2025-12-09 14:47:36 +01:00
Van Leemput Dayron
020fa8823d ci: add Android SDK setup and license acceptance to the Android deployment workflow. 2025-12-09 14:47:04 +01:00
Van Leemput Dayron
959fc33fe4 refactor: use direct shell command for Flutter release APK build.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 34s
2025-12-09 11:31:21 +01:00
Van Leemput Dayron
a57fc811d8 ci: Disable Flutter action cache in Android deployment workflow.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Failing after 50s
2025-12-09 11:27:54 +01:00
Van Leemput Dayron
a96084ba17 ci: Update Ruby version to 3.3 in Android deployment workflow.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Has been cancelled
2025-12-09 11:21:33 +01:00
Van Leemput Dayron
b96c988e80 feat: Set up Fastlane for Android with Firebase App Distribution and adjust build configurations.
Some checks failed
Deploy Flutter to Firebase / deploy-android (push) Has been cancelled
2025-12-09 11:07:09 +01:00
Van Leemput Dayron
14d2761832 ci: Add .env file creation for Google Maps API keys and fix Android package name in deploy workflow.
Some checks failed
Deploy Android to Play Store / build-and-deploy (push) Failing after 26m39s
2025-12-08 21:30:59 +01:00
Van Leemput Dayron
bd3fd28d3c Try resolving CI/CD
Some checks failed
Deploy Android to Play Store / build-and-deploy (push) Failing after 26m33s
2025-12-08 21:01:29 +01:00
Van Leemput Dayron
e8ef20d046 ci: Add Android SDK setup step to the Android deployment workflow.
Some checks failed
Deploy Android to Play Store / build-and-deploy (push) Has been cancelled
2025-12-08 20:48:02 +01:00
Van Leemput Dayron
9fc8d5d1de ci: Add Flutter action cache and remove explicit Flutter version.
Some checks failed
Deploy Android to Play Store / build-and-deploy (push) Failing after 20m38s
2025-12-08 18:20:03 +01:00
Van Leemput Dayron
73db84896a feat: Migrate Android deployment from Jenkins and Fastlane to Gitea Actions workflow.
Some checks failed
Deploy Android to Play Store / build-and-deploy (push) Failing after 4m10s
2025-12-08 18:04:42 +01:00
Van Leemput Dayron
9734532491 style: Fix indentation of Android build and deploy stage in Jenkinsfile 2025-12-08 17:08:35 +01:00
Van Leemput Dayron
7d38f54123 feat: Add retrieval and copying of Flutter .env file from Jenkins credentials for Android build. 2025-12-08 17:06:29 +01:00
Van Leemput Dayron
d02a627b86 fix: Remove Jenkins tools block and hardcode Java path in PATH environment variable. 2025-12-08 16:58:36 +01:00
Van Leemput Dayron
6ce40dd2d6 fix: Consolidate Jenkinsfile PATH environment variable into a single line and clarify related comments for JAVA_HOME and ANDROID_HOME. 2025-12-08 16:51:40 +01:00
Van Leemput Dayron
00ffdcf10b ci: Add JDK 17 tool definition and configure JAVA_HOME in Jenkinsfile. 2025-12-08 16:43:57 +01:00
Van Leemput Dayron
e4d38692fe ci: Add /usr/local/bin to the PATH environment variable in Jenkinsfile. 2025-12-08 16:23:06 +01:00
Van Leemput Dayron
1f93a4e42d ci: Migrate Android Play Store deployment from GitHub Actions to Jenkins using Fastlane. 2025-12-08 16:11:40 +01:00
Van Leemput Dayron
bf48971dc4 feat: Propagate user profile updates to group member details and remove trip code sharing UI.
Some checks failed
Deploy to Play Store / build_and_deploy (push) Has been cancelled
2025-12-06 16:43:40 +01:00
Van Leemput Dayron
13933fc56c Resolve map problem. 2025-12-06 15:50:19 +01:00
25 changed files with 763 additions and 386 deletions

View File

@@ -0,0 +1,55 @@
name: Deploy Flutter to Firebase
on:
push:
branches: release # Vous avez changé la branche ici, c'est noté
jobs:
deploy-android:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: false
- name: Accept Android Licenses
run: yes | flutter doctor --android-licenses || true
- name: Setup Ruby (pour Fastlane)
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Créer le fichier .env
run: echo "${{ secrets.ENV_FILE }}" > .env
- name: Créer le fichier Auth Google
run: echo "${{ secrets.FIREBASE_CREDENTIALS }}" > ./android/firebase_credentials.json
- name: Créer key.properties
run: echo "${{ secrets.ANDROID_KEY_PROPERTIES }}" > ./android/key.properties
- name: Lancer Fastlane Android
working-directory: ./android
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
FIREBASE_ANDROID_APP_ID: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
# --- C'EST ICI LA CORRECTION CRITIQUE POUR LA MÉMOIRE ---
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx1536m"
run: |
bundle install
bundle exec fastlane deploy_firebase

View File

@@ -1,55 +0,0 @@
name: Deploy to Play Store
on:
push:
branches:
- release # L'action se déclenche uniquement sur la branche 'release'
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
# 1. Récupérer le code
- uses: actions/checkout@v4
# 2. Installer Java (requis pour Android build)
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# 3. Installer Flutter
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
# 4. Gérer les dépendances
- run: flutter pub get
# 5. Créer le Keystore depuis le secret (Décodage)
- name: Decode Keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
# 6. Créer le fichier key.properties pour que Gradle trouve la clé
- name: Create key.properties
run: |
echo "storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" > android/key.properties
echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties
echo "storeFile=upload-keystore.jks" >> android/key.properties
# 7. Construire l'AppBundle (.aab)
- name: Build AppBundle
run: flutter build appbundle --release
# 8. Uploader sur le Play Store (Track: alpha = Test fermé)
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_CONFIG_JSON }}
packageName: be.devdayronvl.travel_mate
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: alpha # 'alpha' correspond souvent au Test Fermé. Sinon 'internal' ou 'beta'.
status: completed

6
android/Gemfile Normal file
View File

@@ -0,0 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

241
android/Gemfile.lock Normal file
View File

@@ -0,0 +1,241 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1191.0)
aws-sdk-core (3.239.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.206.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
bigdecimal (3.3.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.229.1)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
naturally (~> 2.2)
nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-firebase_app_distribution (0.10.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-firebaseappdistribution_v1 (0.3.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-firebaseappdistribution_v1alpha (0.2.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.17.1)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.18.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.8.0)
os (1.1.4)
plist (3.7.2)
public_suffix (7.0.0)
rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-25
ruby
DEPENDENCIES
fastlane
fastlane-plugin-firebase_app_distribution
BUNDLED WITH
2.7.2

View File

@@ -30,10 +30,7 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "be.devdayronvl.travel_mate"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
@@ -45,16 +42,18 @@ android {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = if (keystoreProperties["storeFile"] != null) {
file(keystoreProperties["storeFile"] as String)
rootProject.file(keystoreProperties["storeFile"] as String)
} else {
null
}
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
// Applique la configuration de signature définie au-dessus
signingConfig = signingConfigs.getByName("release")
}
}

View File

@@ -21,6 +21,7 @@
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
@@ -62,7 +63,7 @@
android:value="2" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCAtz1_d5K0ANwxAA_T84iq7Ac_gsUs_oM"/>
android:value="AIzaSyAON_ol0Jr34tKbETvdDK9JCQdKNawxBeQ"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />

2
android/fastlane/Appfile Normal file
View File

@@ -0,0 +1,2 @@
json_key_file("'/Users/dayronvanleemput/Documents/Coding/clé/travelmate-a47f5-1e4759031f2d.json'") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("be.devdayronvl.travel_mate") # e.g. com.krausefx.app

28
android/fastlane/Fastfile Normal file
View File

@@ -0,0 +1,28 @@
default_platform(:android)
platform :android do
desc "Déploiement AAB vers Firebase"
lane :deploy_firebase do
# 1. Décoder le Keystore
if ENV['ANDROID_KEYSTORE_BASE64']
File.open("upload-keystore.jks", "wb") do |f|
f.write(Base64.decode64(ENV['ANDROID_KEYSTORE_BASE64']))
end
end
# 2. Construire l'App Bundle (.aab) au lieu de l'APK
# Note : Cela prend souvent un peu plus de RAM
sh("flutter build appbundle --release")
# 3. Envoyer vers Firebase
firebase_app_distribution(
app: ENV["FIREBASE_ANDROID_APP_ID"],
service_credentials_file: "firebase_credentials.json",
groups: "testers",
# --- CHANGEMENT IMPORTANT ICI ---
# On pointe vers le fichier AAB généré
android_artifact_path: "../build/app/outputs/bundle/release/app-release.aab",
release_notes: "Version AAB via Gitea. Commit: #{ENV['GITHUB_SHA']}"
)
end
end

View File

@@ -0,0 +1,5 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-firebase_app_distribution'

View File

@@ -65,15 +65,11 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>LSApplicationQueriesSchemes</key>

View File

@@ -1,6 +1,8 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/repositories/group_repository.dart';
import 'package:travel_mate/services/notification_service.dart';
import 'package:travel_mate/services/logger_service.dart';
import 'package:travel_mate/services/error_service.dart';
@@ -19,6 +21,8 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
/// Firestore instance for user data operations.
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final GroupRepository _groupRepository = GroupRepository();
final _errorService = ErrorService();
/// Creates a new [UserBloc] with initial state.
@@ -164,6 +168,16 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
});
emit(state.UserLoaded(updatedUser));
// Propager les changements aux groupes
await _groupRepository.updateMemberDetails(
userId: currentUser.id,
firstName: event
.userData['prenom'], // 'prenom' dans Firestore map map to firstName usually? Wait, UserModel has prenom/nom.
lastName: event.userData['nom'],
profilePictureUrl: event
.userData['profilePictureUrl'], // Key was 'profilePictureUrl' in ProfileContent
);
} catch (e, stackTrace) {
_errorService.logError(
'UserBloc',

View File

@@ -85,6 +85,18 @@ class AddExpenseDialog extends StatefulWidget {
/// The expense to edit (null for new expense).
final Expense? expenseToEdit;
/// Optional initial category for a new expense.
final ExpenseCategory? initialCategory;
/// Optional initial amount for a new expense.
final double? initialAmount;
/// Optional initial splits (userId -> amount) for a new expense.
final Map<String, double>? initialSplits;
/// Optional initial description for a new expense.
final String? initialDescription;
/// Creates an AddExpenseDialog.
///
/// [group] is the group for the expense.
@@ -95,6 +107,10 @@ class AddExpenseDialog extends StatefulWidget {
required this.group,
required this.currentUser,
this.expenseToEdit,
this.initialCategory,
this.initialAmount,
this.initialSplits,
this.initialDescription,
});
@override
@@ -146,7 +162,10 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
super.initState();
// Initialize form fields and splits based on whether editing or creating
_selectedDate = widget.expenseToEdit?.date ?? DateTime.now();
_selectedCategory = widget.expenseToEdit?.category ?? ExpenseCategory.other;
_selectedCategory =
widget.expenseToEdit?.category ??
widget.initialCategory ??
ExpenseCategory.other;
_selectedCurrency = widget.expenseToEdit?.currency ?? ExpenseCurrency.eur;
_paidById = widget.expenseToEdit?.paidById ?? widget.currentUser.id;
@@ -159,9 +178,32 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
}
_splitEqually = false;
} else {
// Creating: initialize splits for all group members
for (final member in widget.group.members) {
_splits[member.userId] = 0;
// Creating: initialize splits
if (widget.initialDescription != null) {
_descriptionController.text = widget.initialDescription!;
}
if (widget.initialAmount != null) {
_amountController.text = widget.initialAmount.toString();
}
if (widget.initialSplits != null) {
_splits.addAll(widget.initialSplits!);
// Fill remaining members with 0 if not in initialSplits
for (final member in widget.group.members) {
if (!_splits.containsKey(member.userId)) {
_splits[member.userId] = 0;
} else {
// If we have specific splits, we probably aren't splitting equally by default logic
// unless we want to force it. For reimbursement, we likely set exact amounts.
_splitEqually = false;
}
}
} else {
// Default behavior: initialize splits for all group members
for (final member in widget.group.members) {
_splits[member.userId] = 0;
}
}
}
}

View File

@@ -6,7 +6,13 @@
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state;
import '../../models/expense.dart';
import '../../models/group.dart';
import '../../models/user_balance.dart';
import 'add_expense_dialog.dart';
/// A stateless widget that displays a list of user balances in a group.
///
@@ -18,21 +24,19 @@ class BalancesTab extends StatelessWidget {
/// The list of user balances to display.
final List<UserBalance> balances;
/// The group associated with these balances.
final Group group;
/// Creates a `BalancesTab` widget.
///
/// The [balances] parameter must not be null.
const BalancesTab({
super.key,
required this.balances,
});
const BalancesTab({super.key, required this.balances, required this.group});
@override
Widget build(BuildContext context) {
// Check if the balances list is empty and display a placeholder message if true.
if (balances.isEmpty) {
return const Center(
child: Text('Aucune balance à afficher'),
);
return const Center(child: Text('Aucune balance à afficher'));
}
// Render the list of balances as a scrollable list.
@@ -79,81 +83,149 @@ class BalancesTab extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
child: Column(
children: [
// Display the user's initial in a circular avatar.
CircleAvatar(
radius: 24,
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
child: Text(
balance.userName.isNotEmpty
? balance.userName[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 16),
// Display the user's name and financial details.
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User's name.
Text(
balance.userName,
Row(
children: [
// Display the user's initial in a circular avatar.
CircleAvatar(
radius: 24,
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
child: Text(
balance.userName.isNotEmpty
? balance.userName[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 16,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
// User's total paid and owed amounts.
Text(
'Payé: ${balance.totalPaid.toStringAsFixed(2)} € • Doit: ${balance.totalOwed.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
// Display the user's balance status and amount.
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
// Icon indicating the balance status.
Icon(balanceIcon, size: 16, color: balanceColor),
const SizedBox(width: 4),
// User's absolute balance amount.
Text(
'${balance.absoluteBalance.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: balanceColor,
),
const SizedBox(width: 16),
// Display the user's name and financial details.
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User's name.
Text(
balance.userName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
// User's total paid and owed amounts.
Text(
'Payé: ${balance.totalPaid.toStringAsFixed(2)} € • Doit: ${balance.totalOwed.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
// Display the user's balance status and amount.
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
// Icon indicating the balance status.
Icon(balanceIcon, size: 16, color: balanceColor),
const SizedBox(width: 4),
// User's absolute balance amount.
Text(
'${balance.absoluteBalance.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: balanceColor,
),
),
],
),
// Text indicating the balance status (e.g., "À recevoir").
Text(
balanceText,
style: TextStyle(fontSize: 12, color: balanceColor),
),
],
),
// Text indicating the balance status (e.g., "À recevoir").
Text(
balanceText,
style: TextStyle(
fontSize: 12,
color: balanceColor,
),
),
],
),
// "Rembourser" button (Only show if this user is owed money and current user is looking at list?
// Wait, this list shows balances of everyone.
// Requirement: "Il faut un bouton dans la page qui permet de régler l'argent qu'on doit à une certaine personne"
// So if I look at "Alice", and Alice "shouldReceive" (is green), it implies the group owes Alice.
// But does it mean *I* owe Alice?
// The BalancesTab shows the *Group's* balances.
// However, usually settlement is 1-on-1. The requirement says: "régler l'argent qu'on doit à une certaine personne".
// If the user displayed here 'shouldReceive' money, it means they are owed money.
// If I click 'Rembourser', it implies *I* am paying them.
// This button should probably be available if the user on the card is POSITIVE (shouldReceive)
// AND I am not that user.
if (balance.shouldReceive) ...[
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showReimbursementDialog(context, balance),
icon: const Icon(Icons.monetization_on_outlined),
label: Text('Rembourser ${balance.userName}'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.green,
side: const BorderSide(color: Colors.green),
),
),
),
],
],
),
),
);
}
void _showReimbursementDialog(
BuildContext context,
UserBalance payeeBalance,
) {
final userState = context.read<UserBloc>().state;
if (userState is! user_state.UserLoaded) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Erreur: utilisateur non connecté')),
);
return;
}
final currentUser = userState.user;
// Prevent reimbursing yourself
if (payeeBalance.userId == currentUser.id) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Vous ne pouvez pas vous rembourser vous-même'),
),
);
return;
}
showDialog(
context: context,
builder: (context) => AddExpenseDialog(
group: group,
currentUser: currentUser,
initialCategory: ExpenseCategory.reimbursement,
initialDescription: 'Remboursement',
initialAmount: payeeBalance.absoluteBalance,
initialSplits: {
payeeBalance.userId: payeeBalance
.absoluteBalance, // The payee receives the full amount (as split)
},
),
);
}
}

View File

@@ -194,6 +194,8 @@ class ExpensesTab extends StatelessWidget {
return Colors.teal;
case ExpenseCategory.other:
return Colors.grey;
case ExpenseCategory.reimbursement:
return Colors.green;
}
}

View File

@@ -80,14 +80,7 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () {
_showFilterDialog();
},
),
],
actions: [],
),
body: MultiBlocListener(
listeners: [
@@ -193,7 +186,10 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
if (state is BalanceLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is GroupBalancesLoaded) {
return BalancesTab(balances: state.balances);
return BalancesTab(
balances: state.balances,
group: widget.group,
);
} else if (state is BalanceError) {
return _buildErrorState('Erreur: ${state.message}');
}
@@ -390,96 +386,4 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
ErrorService().showError(message: 'Erreur: utilisateur non connecté');
}
}
void _showFilterDialog() {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('Filtrer les dépenses'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<ExpenseCategory>(
// ignore: deprecated_member_use
value: _selectedCategory,
decoration: const InputDecoration(
labelText: 'Catégorie',
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem<ExpenseCategory>(
value: null,
child: Text('Toutes'),
),
...ExpenseCategory.values.map((category) {
return DropdownMenuItem(
value: category,
child: Text(category.displayName),
);
}),
],
onChanged: (value) {
setState(() => _selectedCategory = value);
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
// ignore: deprecated_member_use
value: _selectedPayerId,
decoration: const InputDecoration(
labelText: 'Payé par',
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem<String>(
value: null,
child: Text('Tous'),
),
...widget.group.members.map((member) {
return DropdownMenuItem(
value: member.userId,
child: Text(member.firstName),
);
}),
],
onChanged: (value) {
setState(() => _selectedPayerId = value);
},
),
],
),
actions: [
TextButton(
onPressed: () {
setState(() {
_selectedCategory = null;
_selectedPayerId = null;
});
// Also update parent state
this.setState(() {
_selectedCategory = null;
_selectedPayerId = null;
});
Navigator.pop(context);
},
child: const Text('Réinitialiser'),
),
ElevatedButton(
onPressed: () {
// Update parent state
this.setState(() {});
Navigator.pop(context);
},
child: const Text('Appliquer'),
),
],
);
},
);
},
);
}
}

View File

@@ -26,6 +26,8 @@ import 'package:travel_mate/blocs/activity/activity_state.dart';
import 'package:travel_mate/blocs/balance/balance_bloc.dart';
import 'package:travel_mate/blocs/balance/balance_event.dart';
import 'package:travel_mate/blocs/balance/balance_state.dart';
import 'package:travel_mate/blocs/group/group_bloc.dart';
import 'package:travel_mate/blocs/group/group_event.dart';
import 'package:travel_mate/blocs/user/user_bloc.dart';
import 'package:travel_mate/blocs/user/user_state.dart' as user_state;
@@ -641,38 +643,20 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
),
const Divider(),
],
ListTile(
leading: Icon(
Icons.share,
color: theme.colorScheme.onSurface,
),
title: Text(
'Partager le code',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface,
if (!isCreator)
ListTile(
leading: Icon(Icons.exit_to_app, color: Colors.red[400]),
title: Text(
'Quitter le voyage',
style: theme.textTheme.bodyLarge?.copyWith(
color: Colors.red[400],
),
),
onTap: () {
Navigator.pop(context);
_handleLeaveTrip(currentUser);
},
),
onTap: () {
Navigator.pop(context);
// Implement share functionality
if (_group != null) {
// Use share_plus package to share the code
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('ID du groupe : ${_group!.id}'),
action: SnackBarAction(
label: 'Copier',
onPressed: () {
Clipboard.setData(
ClipboardData(text: _group!.id),
);
},
),
),
);
}
},
),
],
),
);
@@ -682,6 +666,91 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
);
}
void _handleLeaveTrip(user_state.UserModel? currentUser) {
if (currentUser == null || _group == null) return;
// Vérifier les dettes
final balanceState = context.read<BalanceBloc>().state;
if (balanceState is GroupBalancesLoaded) {
final myBalance = balanceState.balances.firstWhere(
(b) => b.userId == currentUser.id,
orElse: () => const UserBalance(
userId: '',
userName: '',
totalPaid: 0,
totalOwed: 0,
balance: 0,
),
);
// Tolérance pour les arrondis (0.01€)
if (myBalance.balance.abs() > 0.01) {
_errorService.showError(
message:
'Vous devez régler vos dettes (ou récupérer votre argent) avant de quitter le voyage. Solde: ${myBalance.formattedBalance}',
);
return;
}
_confirmLeaveTrip(currentUser.id);
} else {
// Si les balances ne sont pas chargées, on essaie de les charger et on demande de rééssayer
context.read<BalanceBloc>().add(LoadGroupBalances(_group!.id));
_errorService.showError(
message:
'Impossible de vérifier votre solde. Veuillez réessayer dans un instant.',
);
}
}
void _confirmLeaveTrip(String userId) {
final theme = Theme.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor:
theme.dialogTheme.backgroundColor ?? theme.colorScheme.surface,
title: Text(
'Quitter le voyage',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
content: Text(
'Êtes-vous sûr de vouloir quitter ce voyage ? Vous ne pourrez plus voir les détails ni les dépenses.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: TextStyle(color: theme.colorScheme.primary),
),
),
TextButton(
onPressed: () {
Navigator.pop(context); // Fermer le dialog
if (_group != null) {
context.read<GroupBloc>().add(
RemoveMemberFromGroup(_group!.id, userId),
);
// Retourner à l'écran d'accueil
Navigator.pop(context);
}
},
child: const Text('Quitter', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _confirmDeleteTrip() {
final theme = Theme.of(context);

View File

@@ -41,12 +41,9 @@ class _TripCardState extends State<TripCard> {
});
try {
// D'abord vérifier si une image existe déjà dans le Storage
String? imageUrl = await _placeImageService.getExistingImageUrl(
widget.trip.location,
);
// Si aucune image n'existe, en télécharger une nouvelle
imageUrl ??= await _placeImageService.getPlaceImageUrl(
widget.trip.location,
);
@@ -57,7 +54,6 @@ class _TripCardState extends State<TripCard> {
_isLoadingImage = false;
});
// Mettre à jour le voyage dans la base de données avec l'imageUrl
_updateTripWithImage(imageUrl);
} else {
setState(() {
@@ -76,18 +72,13 @@ class _TripCardState extends State<TripCard> {
Future<void> _updateTripWithImage(String imageUrl) async {
try {
if (widget.trip.id != null) {
// Créer une copie du voyage avec la nouvelle imageUrl
final updatedTrip = widget.trip.copyWith(
imageUrl: imageUrl,
updatedAt: DateTime.now(),
);
// Mettre à jour dans la base de données
await _tripRepository.updateTrip(widget.trip.id!, updatedTrip);
}
} catch (e) {
// En cas d'erreur, on continue sans échec - l'image reste affichée localement
}
} catch (e) {}
}
Widget _buildImageWidget() {

View File

@@ -325,14 +325,14 @@ class _MapContentState extends State<MapContent> {
Future<BitmapDescriptor> _createCustomMarkerIcon() async {
final pictureRecorder = ui.PictureRecorder();
final canvas = Canvas(pictureRecorder);
const size = 120.0;
const size = 80.0;
// Dessiner l'icône person_pin_circle en bleu
final iconPainter = TextPainter(textDirection: TextDirection.ltr);
iconPainter.text = TextSpan(
text: String.fromCharCode(Icons.person_pin_circle.codePoint),
style: TextStyle(
fontSize: 100,
fontSize: 70,
fontFamily: Icons.person_pin_circle.fontFamily,
color: Colors.blue[700],
),

View File

@@ -11,6 +11,13 @@ class PoliciesContent extends StatelessWidget {
}
}
Future<void> _launchCustomPrivacyPolicy() async {
final Uri url = Uri.parse('https://xeewy.be/travelmate/policies');
if (!await launchUrl(url)) {
throw Exception('Could not launch $url');
}
}
Future<void> _onBackPressed(BuildContext context) async {
if (Navigator.canPop(context)) {
Navigator.pop(context);
@@ -44,108 +51,8 @@ class PoliciesContent extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Collecte d'informations
Text(
'Collecte d\'informations',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Nous collectons des informations que vous nous fournissez directement, comme votre nom, adresse e-mail et préférences de voyage.',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
// Keep only the buttons
const SizedBox(height: 20),
// Section Utilisation des données
Text(
'Utilisation des données',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Vos données sont utilisées pour améliorer votre expérience utilisateur et vous proposer des recommandations personnalisées.',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 20),
// Section Protection des données
Text(
'Protection des données',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Nous mettons en place des mesures de sécurité appropriées pour protéger vos informations personnelles.',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 20),
// Section Partage des données
Text(
'Partage des données',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Nous ne partageons pas vos informations personnelles avec des tiers sans votre consentement explicite.',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 20),
// Section Droits de l'utilisateur
Text(
'Vos droits',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Vous avez le droit d\'accéder, de corriger ou de supprimer vos données personnelles à tout moment. Veuillez nous contacter pour toute demande.',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 20),
// Section Contact
Text(
'Nous contacter',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 10),
Text(
'Pour toute question concernant cette politique de confidentialité, veuillez nous contacter à support@travelmate.com',
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 20),
// Bouton Google Privacy Policy
SizedBox(
width: double.infinity,
@@ -163,6 +70,24 @@ class PoliciesContent extends StatelessWidget {
),
),
),
const SizedBox(height: 12),
// Bouton Nos politiques de confidentialité
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _launchCustomPrivacyPolicy,
icon: const Icon(Icons.policy),
label: const Text('Nos politiques de confidentialités'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(height: 32),
],
),

View File

@@ -3,9 +3,33 @@ import 'package:travel_mate/components/settings/policies/policies_content.dart';
import 'theme/settings_theme_content.dart';
import 'profile/profile_content.dart';
class SettingsContent extends StatelessWidget {
import 'package:package_info_plus/package_info_plus.dart';
class SettingsContent extends StatefulWidget {
const SettingsContent({super.key});
@override
State<SettingsContent> createState() => _SettingsContentState();
}
class _SettingsContentState extends State<SettingsContent> {
String _version = '...';
@override
void initState() {
super.initState();
_loadVersion();
}
Future<void> _loadVersion() async {
final packageInfo = await PackageInfo.fromPlatform();
if (mounted) {
setState(() {
_version = packageInfo.version;
});
}
}
Future<void> changePage(BuildContext context, Widget page) async {
Navigator.push(context, MaterialPageRoute(builder: (context) => page));
}
@@ -70,7 +94,7 @@ class SettingsContent extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'1.0.0',
_version,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:travel_mate/blocs/balance/balance_bloc.dart';
import 'package:travel_mate/blocs/expense/expense_bloc.dart';
import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:travel_mate/blocs/message/message_bloc.dart';
import 'package:travel_mate/blocs/activity/activity_bloc.dart';
import 'package:travel_mate/firebase_options.dart';
@@ -45,6 +48,10 @@ import 'package:intl/date_symbol_data_local.dart';
/// initializes Firebase, and starts the application.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
await dotenv.load(fileName: ".env");
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await initializeDateFormatting('fr_FR', null);
@@ -54,6 +61,13 @@ void main() async {
await NotificationService().initialize();
// Requirements for Google Maps on Android (Hybrid Composition)
final GoogleMapsFlutterPlatform mapsImplementation =
GoogleMapsFlutterPlatform.instance;
if (mapsImplementation is GoogleMapsFlutterAndroid) {
mapsImplementation.useAndroidViewSurface = true;
}
runApp(const MyApp());
}

View File

@@ -45,7 +45,10 @@ enum ExpenseCategory {
shopping('Shopping', Icons.shopping_bag),
/// Other miscellaneous expenses
other('Other', Icons.category);
other('Other', Icons.category),
/// Reimbursement for settling debts
reimbursement('Remboursement', Icons.monetization_on);
const ExpenseCategory(this.displayName, this.icon);

View File

@@ -286,4 +286,42 @@ class GroupRepository {
.toList(),
);
}
Future<void> updateMemberDetails({
required String userId,
String? firstName,
String? lastName,
String? profilePictureUrl,
}) async {
try {
// 1. Trouver tous les groupes où l'utilisateur est membre
final groupsSnapshot = await _groupsCollection
.where('memberIds', arrayContains: userId)
.get();
// 2. Mettre à jour les infos du membre dans chaque groupe
for (final groupDoc in groupsSnapshot.docs) {
final memberRef = _membersCollection(groupDoc.id).doc(userId);
final updates = <String, dynamic>{};
if (firstName != null) updates['firstName'] = firstName;
if (lastName != null) updates['lastName'] = lastName;
if (profilePictureUrl != null) {
updates['profilePictureUrl'] = profilePictureUrl;
}
if (updates.isNotEmpty) {
await memberRef.update(updates);
}
}
} catch (e, stackTrace) {
_errorService.logError(
'GroupRepository',
'Erreur update member details: $e',
stackTrace,
);
// On ne throw pas d'exception ici pour ne pas bloquer l'update user principal
// C'est une opération "best effort"
}
}
}

View File

@@ -1089,7 +1089,7 @@ packages:
source: hosted
version: "2.2.0"
package_info_plus:
dependency: transitive
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.1+2
version: 1.0.2+3
environment:
sdk: ^3.9.2
@@ -63,6 +63,7 @@ dependencies:
image: ^4.5.4
firebase_messaging: ^16.0.4
flutter_local_notifications: ^19.5.0
package_info_plus: ^8.3.1
dev_dependencies:
flutter_launcher_icons: ^0.13.1