├── .env.example ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── app │ │ │ │ └── mygrid │ │ │ │ └── grid │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-night-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-night │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night-v31 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── AppIcons │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 102.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 20.png │ │ │ ├── 216.png │ │ │ ├── 256.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 66.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 92.png │ │ │ └── Contents.json │ ├── android │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ ├── appstore.png │ └── playstore.png ├── fonts │ └── fontawesome-webfont.ttf ├── logos │ ├── Brand-Pattern-Full-Color.png │ ├── Brand-Pattern-Low-color.png │ ├── Color codes.pdf │ ├── Jpeg-2.jpg │ ├── Jpeg.jpg │ ├── Pdf file 2.pdf │ ├── Png-file.png │ ├── Source file.ai │ ├── Svg file 2.svg │ ├── Svg file.svg │ ├── pdf file.pdf │ ├── png-file-2.png │ ├── vector file.ai │ └── vector o2.ai └── lottie │ ├── lottie-phone-pin.json │ └── lottie-phone-verify.json ├── devtools_options.yaml ├── flutter_native_splash.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── LaunchBackground.imageset │ │ │ ├── Contents.json │ │ │ ├── background.png │ │ │ └── darkbackground.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ ├── playstore 1.png │ │ │ ├── playstore 2.png │ │ │ └── playstore.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── Runner.entitlements └── RunnerTests │ └── RunnerTests.swift ├── lib ├── blocs │ ├── contacts │ │ ├── contacts_bloc.dart │ │ ├── contacts_event.dart │ │ └── contacts_state.dart │ ├── groups │ │ ├── groups_bloc.dart │ │ ├── groups_event.dart │ │ └── groups_state.dart │ └── map │ │ ├── map_bloc.dart │ │ ├── map_event.dart │ │ └── map_state.dart ├── components │ └── modals │ │ └── notice_continue_modal.dart ├── main.dart ├── models │ ├── contact_display.dart │ ├── grid_user.dart │ ├── pending_message.dart │ ├── room.dart │ ├── sharing_preferences.dart │ ├── sharing_window.dart │ └── user_location.dart ├── providers │ ├── auth_provider.dart │ ├── contacts_refresh_provider.dart │ ├── selected_subscreen_provider.dart │ ├── selected_user_provider.dart │ └── user_location_provider.dart ├── repositories │ ├── location_repository.dart │ ├── room_repository.dart │ ├── sharing_preferences_repository.dart │ ├── user_keys_repository.dart │ └── user_repository.dart ├── screens │ ├── map │ │ └── map_tab.dart │ ├── onboarding │ │ ├── login_screen.dart │ │ ├── server_select_screen.dart │ │ ├── signup_screen.dart │ │ ├── splash_screen.dart │ │ ├── username_select_screen.dart │ │ └── welcome_screen.dart │ └── settings │ │ ├── notifications_settings.dart │ │ └── settings_page.dart ├── services │ ├── android_background_task.dart │ ├── backwards_compatibility_service.dart │ ├── database_service.dart │ ├── location_manager.dart │ ├── matrix_service.dart │ ├── message_processor.dart │ ├── room_service.dart │ ├── sync_manager.dart │ └── user_service.dart ├── styles │ └── themes.dart ├── utilities │ ├── encryption_utils.dart │ ├── message_parser.dart │ ├── time_ago_formatter.dart │ └── utils.dart └── widgets │ ├── add_friend_modal.dart │ ├── add_group_member_modal.dart │ ├── add_sharing_preferences_modal.dart │ ├── app_initializer.dart │ ├── contact_profile_modal.dart │ ├── contacts_subscreen.dart │ ├── custom_search_bar.dart │ ├── friend_request_modal.dart │ ├── group_details_subscreen.dart │ ├── group_info_subscreen.dart │ ├── group_invitation_modal.dart │ ├── group_profile_modal.dart │ ├── groups_subscreen.dart │ ├── invites_modal.dart │ ├── map_scroll_window.dart │ ├── onboarding_modal.dart │ ├── profile_modal.dart │ ├── status_indictator.dart │ ├── triangle_avatars.dart │ ├── two_user_avatars.dart │ ├── user_info_bubble.dart │ ├── user_keys_modal.dart │ ├── user_map_marker.dart │ ├── version_checker.dart │ └── version_wrapper.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.env.example: -------------------------------------------------------------------------------- 1 | # .env file 2 | MATRIX_SERVER_URL= 3 | GAUTH_URL= 4 | HOMESERVER= 5 | MAPS_URL= 6 | VERSION_CHECK_URL= 7 | APP_STORE_URL= 8 | PLAY_STORE_URL= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | *.env 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | #iOS Builds 48 | ios/build 49 | ios/build* 50 | 51 | #Android Builds 52 | android/build/* 53 | android/build 54 | *.jks -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "300451adae589accbece3490f4396f10bdf15e6e" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 17 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 18 | - platform: android 19 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 20 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 21 | - platform: ios 22 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 23 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 24 | - platform: linux 25 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 26 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 27 | - platform: macos 28 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 29 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 30 | - platform: web 31 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 32 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 33 | - platform: windows 34 | create_revision: 300451adae589accbece3490f4396f10bdf15e6e 35 | base_revision: 300451adae589accbece3490f4396f10bdf15e6e 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2024 Chandler Gach 5 | "Grid" App - An end-to-end encrypted location-sharing application. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The GNU Affero General Public License is a free, copyleft license for 13 | software and other kinds of works, specifically designed to ensure 14 | cooperation with the community in the case of network server software. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | our General Public Licenses are intended to guarantee your freedom to 19 | share and change all versions of a program--to make sure it remains 20 | free software for all its users. 21 | 22 | This license, like the GNU General Public License, is a copyleft 23 | license, which means that derivative works of the program must be free 24 | software and carry the same terms. In this case, when you run a 25 | modified program on a server and let other users interact with it, the 26 | AGPL requires you to make the source code of your modified version 27 | available to those users. These requirements apply to the modified 28 | version as well as any associated programs that interact with it. 29 | 30 | You are free to use this software under the following conditions: 31 | 32 | - The code is made available under the AGPL-3.0 license. 33 | - Any modifications made to the code must also be made publicly available under the same license. 34 | - If you make the program available to users through a network (for example, as a web application), you must make the modified source code available to those users under the AGPL-3.0. 35 | 36 | You may use this software as-is, without warranty of any kind, and you may redistribute it and modify it as long as you comply with the conditions of the AGPL-3.0 license. 37 | 38 | For the full text of the license, see . 39 | 40 | END OF TERMS AND CONDITIONS 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Grid - Encrypted Location Sharing

3 |

Be Hard to Track.

4 |
5 | Logo Grid 6 |
7 |
8 |
9 | mygrid.app 10 |
11 | 12 | 13 |
14 | 15 |
16 | GitHub Repo stars 17 | Twitter Follow 18 | License 19 |
20 |
21 | 22 | ***Grid*** is a secure, end-to-end encrypted (E2EE) location sharing application integrated with the Matrix Protocol. Built using Flutter, Grid provides a privacy-focused solution for sharing your location with trusted contacts. 23 | 24 | 25 | 26 |
27 | appstore 28 |
29 | 30 | 31 | ## Features 32 | 33 | - **End-to-End Encryption (E2EE)**: All location data shared through Grid is encrypted to ensure privacy and security. 34 | - **Matrix Protocol Integration**: Grid leverages the Matrix protocol for secure communication and decentralized data storage. 35 | - **Cross-Platform**: Developed with Flutter, Grid runs seamlessly on both Android and iOS devices. 36 | - **Real-Time Location Sharing**: Share your real-time location with friends or groups, with fine-grained control over who can see your location. 37 | - **Self-Hosted Capability** Grid is designed to enable users to easily self host their own backend server and map tile provider for complete control over how they share. 38 | 39 | 40 | ## Roadmap 41 | 42 |
43 | appstore 44 |
45 | 46 | ## Getting Started With the App 47 | If you wish to develop/contribute PRs to application, follow the steps below: 48 | ### Prerequisites 49 | 50 | Before you begin, ensure you have the following installed: 51 | 52 | - **Flutter SDK**: [Install Flutter](https://flutter.dev/docs/get-started/install) 53 | - **Android Studio**: [Download Android Studio](https://developer.android.com/studio) 54 | - **Xcode** (for iOS development): [Install Xcode](https://developer.apple.com/xcode/) 55 | - **CocoaPods** (for iOS): [Install CocoaPods](https://guides.cocoapods.org/using/getting-started.html) 56 | 57 | ### Installation 58 | 59 | 1. **Clone the repository**: 60 | 61 | ```bash 62 | git clone https://github.com/Rezivure/grid-frontend.git 63 | cd grid-frontend 64 | ``` 65 | 66 | 2. **Install dependencies**: 67 | 68 | Run the following command to install the necessary dependencies: 69 | 70 | ```bash 71 | flutter pub get 72 | ``` 73 | 74 | 3. **Set up environment variables**: 75 | 76 | Copy the example environment configuration and modify it with the appropriate URLs: 77 | 78 | ```bash 79 | cp .env.example .env 80 | ``` 81 | Edit `.env` to configure your API and server URLs. 82 | 83 | 4. **Platform-specific setup**: 84 | 85 | #### For iOS: 86 | 87 | - Navigate to the `ios/` directory: 88 | 89 | ```bash 90 | cd ios 91 | ``` 92 | 93 | - Install CocoaPods dependencies: 94 | 95 | ```bash 96 | pod install 97 | ``` 98 | 99 | - Return to the root directory: 100 | 101 | ```bash 102 | cd .. 103 | ``` 104 | 105 | #### For Android: 106 | 107 | No additional setup is required. 108 | 109 | ### Running the App 110 | 111 | 1. **Open Android Studio**: 112 | 113 | - Open the cloned repository in Android Studio. 114 | - Ensure your Flutter SDK is correctly set up in Android Studio. 115 | 116 | 2. **Set Up Emulator or Physical Device**: 117 | 118 | - Create an Android Emulator or connect a physical device. 119 | - Ensure the device is running and detected by Android Studio. 120 | 121 | 3. **Run the App**: 122 | 123 | Use the following command in the terminal to build and run the app on the connected device or emulator: 124 | 125 | ```bash 126 | flutter run 127 | ``` 128 | 129 | ## Project Structure 130 | 131 | - **lib/**: Contains the main Flutter application code. 132 | - **assets/**: Stores images, icons, and other assets. 133 | - **pubspec.yaml**: Defines the dependencies and assets for the project. 134 | 135 | ## Contributing 136 | 137 | We welcome contributions! To do so, please reference our Contribution Guidelines [here](https://docs.mygrid.app/docs/category/contributing-to-grid)! 138 | 139 | ## License 140 | 141 | This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE](./LICENSE) file for details. 142 | 143 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | // Load local.properties 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file('local.properties') 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader('UTF-8') { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | // Provide defaults if not found in local.properties 17 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode', '1') 18 | def flutterVersionName = localProperties.getProperty('flutter.versionName', '1.0') 19 | 20 | // flutter_background_geolocation 21 | Project background_geolocation = project(':flutter_background_geolocation') 22 | apply from: "${background_geolocation.projectDir}/background_geolocation.gradle" 23 | 24 | android { 25 | namespace "app.mygrid.grid" 26 | compileSdkVersion rootProject.ext.compileSdkVersion 27 | ndkVersion flutter.ndkVersion 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_17 31 | targetCompatibility JavaVersion.VERSION_17 32 | coreLibraryDesugaringEnabled true 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '17' 37 | } 38 | 39 | defaultConfig { 40 | applicationId "app.mygrid.grid" 41 | minSdkVersion rootProject.ext.minSdkVersion 42 | targetSdkVersion rootProject.ext.targetSdkVersion 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | manifestPlaceholders = [applicationName: "android.app.Application", 46 | transistorsoftKey: localProperties.getProperty('transistorsoft.key', '') 47 | ] 48 | } 49 | 50 | 51 | signingConfigs { 52 | release { 53 | keyAlias localProperties.getProperty('keyAlias') 54 | keyPassword localProperties.getProperty('keyPassword') 55 | storeFile file(localProperties.getProperty('storeFile')) 56 | storePassword localProperties.getProperty('storePassword') 57 | } 58 | } 59 | 60 | buildTypes { 61 | release { 62 | signingConfig signingConfigs.release 63 | minifyEnabled true 64 | shrinkResources false 65 | } 66 | } 67 | } 68 | 69 | flutter { 70 | source '../..' 71 | } 72 | 73 | dependencies { 74 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 75 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3" 76 | 77 | } 78 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 23 | 24 | 25 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/app/mygrid/grid/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package app.mygrid.grid 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-night-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-night/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.10' // Keep it stable and known-working 3 | ext { 4 | compileSdkVersion = 34 5 | targetSdkVersion = 34 6 | minSdkVersion = 21 7 | appCompatVersion = "1.4.2" 8 | playServicesLocationVersion = "21.0.1" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath "com.android.tools.build:gradle:8.2.1" 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | maven { url "${project(':flutter_background_geolocation').projectDir}/libs" } 25 | maven { url 'https://developer.huawei.com/repo/' } 26 | maven { url "${project(':background_fetch').projectDir}/libs" } 27 | maven { url "https://storage.googleapis.com/download.flutter.io" } 28 | } 29 | } 30 | 31 | 32 | rootProject.buildDir = '../build' 33 | subprojects { 34 | project.buildDir = "${rootProject.buildDir}/${project.name}" 35 | project.evaluationDependsOn(':app') 36 | } 37 | 38 | tasks.register("clean", Delete) { 39 | delete rootProject.buildDir 40 | } 41 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "8.2.1" apply false 23 | id "org.jetbrains.kotlin.android" version "1.9.10" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/102.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/Assets.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /assets/AppIcons/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"102.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"45x45","expected-size":"102","role":"appLauncher"},{"idiom":"watch","filename":"92.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"46x46","expected-size":"92","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"idiom":"watch","filename":"66.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"33x33","expected-size":"66","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /assets/AppIcons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /assets/AppIcons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /assets/AppIcons/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/appstore.png -------------------------------------------------------------------------------- /assets/AppIcons/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/AppIcons/playstore.png -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/logos/Brand-Pattern-Full-Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Brand-Pattern-Full-Color.png -------------------------------------------------------------------------------- /assets/logos/Brand-Pattern-Low-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Brand-Pattern-Low-color.png -------------------------------------------------------------------------------- /assets/logos/Color codes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Color codes.pdf -------------------------------------------------------------------------------- /assets/logos/Jpeg-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Jpeg-2.jpg -------------------------------------------------------------------------------- /assets/logos/Jpeg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Jpeg.jpg -------------------------------------------------------------------------------- /assets/logos/Pdf file 2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Pdf file 2.pdf -------------------------------------------------------------------------------- /assets/logos/Png-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Png-file.png -------------------------------------------------------------------------------- /assets/logos/Source file.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/Source file.ai -------------------------------------------------------------------------------- /assets/logos/Svg file 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/logos/Svg file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/logos/pdf file.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/pdf file.pdf -------------------------------------------------------------------------------- /assets/logos/png-file-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/png-file-2.png -------------------------------------------------------------------------------- /assets/logos/vector file.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/vector file.ai -------------------------------------------------------------------------------- /assets/logos/vector o2.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/assets/logos/vector o2.ai -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /flutter_native_splash.yaml: -------------------------------------------------------------------------------- 1 | flutter_native_splash: 2 | # Native splash with just background colors to seamlessly transition 3 | # to the Flutter splash screen 4 | 5 | # Light mode configuration 6 | color: "#FFFFFF" # White background for light mode 7 | # No image - just blank background 8 | 9 | # Dark mode configuration 10 | color_dark: "#191919" # Dark background (matching RGB: 25/25/25) 11 | # No image - just blank background 12 | 13 | # Android specific settings 14 | android: true 15 | android_12: 16 | # Android 12+ uses a new splash screen API 17 | color: "#FFFFFF" 18 | color_dark: "#191919" 19 | # No image for Android 12+ either 20 | 21 | # iOS specific settings 22 | ios: true 23 | 24 | # Ensure the splash screen fills the entire screen 25 | fullscreen: true 26 | 27 | # Web configuration (if needed in future) 28 | web: false -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | # Existing Flutter build settings 42 | installer.pods_project.targets.each do |target| 43 | flutter_additional_ios_build_settings(target) 44 | end 45 | 46 | # Bitcode stripping logic 47 | bitcode_strip_path = `xcrun --find bitcode_strip`.chop! 48 | 49 | def strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path) 50 | framework_path = File.join(Dir.pwd, framework_relative_path) 51 | command = "#{bitcode_strip_path} #{framework_path} -r -o #{framework_path}" 52 | puts "Stripping bitcode: #{command}" 53 | system(command) 54 | end 55 | 56 | framework_paths = [ 57 | "Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64_armv7/OpenSSL.framework/OpenSSL" 58 | ] 59 | 60 | framework_paths.each do |framework_relative_path| 61 | strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - background_fetch (1.3.7): 3 | - Flutter 4 | - CocoaLumberjack (3.8.5): 5 | - CocoaLumberjack/Core (= 3.8.5) 6 | - CocoaLumberjack/Core (3.8.5) 7 | - Flutter (1.0.0) 8 | - flutter_background_geolocation (4.16.6): 9 | - CocoaLumberjack (~> 3.8.5) 10 | - Flutter 11 | - flutter_compass (0.0.1): 12 | - Flutter 13 | - flutter_native_splash (2.4.3): 14 | - Flutter 15 | - flutter_olm (3.2.15): 16 | - Flutter 17 | - flutter_openssl_crypto (0.0.1): 18 | - Flutter 19 | - OpenSSL-Universal 20 | - flutter_secure_storage (6.0.0): 21 | - Flutter 22 | - geolocator_apple (1.2.0): 23 | - Flutter 24 | - MTBBarcodeScanner (5.0.11) 25 | - OpenSSL-Universal (1.1.1100) 26 | - package_info_plus (0.4.5): 27 | - Flutter 28 | - path_provider_foundation (0.0.1): 29 | - Flutter 30 | - FlutterMacOS 31 | - permission_handler_apple (9.3.0): 32 | - Flutter 33 | - qr_code_scanner_plus (0.2.6): 34 | - Flutter 35 | - MTBBarcodeScanner 36 | - shared_preferences_foundation (0.0.1): 37 | - Flutter 38 | - FlutterMacOS 39 | - sqflite_darwin (0.0.4): 40 | - Flutter 41 | - FlutterMacOS 42 | - url_launcher_ios (0.0.1): 43 | - Flutter 44 | 45 | DEPENDENCIES: 46 | - background_fetch (from `.symlinks/plugins/background_fetch/ios`) 47 | - Flutter (from `Flutter`) 48 | - flutter_background_geolocation (from `.symlinks/plugins/flutter_background_geolocation/ios`) 49 | - flutter_compass (from `.symlinks/plugins/flutter_compass/ios`) 50 | - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) 51 | - flutter_olm (from `.symlinks/plugins/flutter_olm/ios`) 52 | - flutter_openssl_crypto (from `.symlinks/plugins/flutter_openssl_crypto/ios`) 53 | - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) 54 | - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) 55 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 56 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 57 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 58 | - qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`) 59 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 60 | - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 61 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 62 | 63 | SPEC REPOS: 64 | trunk: 65 | - CocoaLumberjack 66 | - MTBBarcodeScanner 67 | - OpenSSL-Universal 68 | 69 | EXTERNAL SOURCES: 70 | background_fetch: 71 | :path: ".symlinks/plugins/background_fetch/ios" 72 | Flutter: 73 | :path: Flutter 74 | flutter_background_geolocation: 75 | :path: ".symlinks/plugins/flutter_background_geolocation/ios" 76 | flutter_compass: 77 | :path: ".symlinks/plugins/flutter_compass/ios" 78 | flutter_native_splash: 79 | :path: ".symlinks/plugins/flutter_native_splash/ios" 80 | flutter_olm: 81 | :path: ".symlinks/plugins/flutter_olm/ios" 82 | flutter_openssl_crypto: 83 | :path: ".symlinks/plugins/flutter_openssl_crypto/ios" 84 | flutter_secure_storage: 85 | :path: ".symlinks/plugins/flutter_secure_storage/ios" 86 | geolocator_apple: 87 | :path: ".symlinks/plugins/geolocator_apple/ios" 88 | package_info_plus: 89 | :path: ".symlinks/plugins/package_info_plus/ios" 90 | path_provider_foundation: 91 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 92 | permission_handler_apple: 93 | :path: ".symlinks/plugins/permission_handler_apple/ios" 94 | qr_code_scanner_plus: 95 | :path: ".symlinks/plugins/qr_code_scanner_plus/ios" 96 | shared_preferences_foundation: 97 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 98 | sqflite_darwin: 99 | :path: ".symlinks/plugins/sqflite_darwin/darwin" 100 | url_launcher_ios: 101 | :path: ".symlinks/plugins/url_launcher_ios/ios" 102 | 103 | SPEC CHECKSUMS: 104 | background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 105 | CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0 106 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 107 | flutter_background_geolocation: 0c6320679295ce63872295d85de144bfffbfc4b6 108 | flutter_compass: cbbd285cea1584c7ac9c4e0c3e1f17cbea55e855 109 | flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a 110 | flutter_olm: 41bad3c821821a227ce3b793d338e9801fb8d0ae 111 | flutter_openssl_crypto: 636ad25f56fbe0f45b9e13b8589540b341a486a1 112 | flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 113 | geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 114 | MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb 115 | OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c 116 | package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 117 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 118 | permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 119 | qr_code_scanner_plus: 3bfe4deb7f28996a63a2a580819d49dae80d5ed3 120 | shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 121 | sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 122 | url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 123 | 124 | PODFILE CHECKSUM: e951dee4c1394c54580f6aa43f9639e8904d95ae 125 | 126 | COCOAPODS: 1.16.2 127 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 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"}} -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "darkbackground.png", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore 2.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/ios/Runner/Assets.xcassets/LaunchImage.imageset/playstore.png -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Grid 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Grid 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSCameraUsageDescription 30 | We need camera access to scan QR codes. 31 | NSMotionUsageDescription 32 | Grid needs access to your motion to efficiently update your location, such as determining whether you are walking, driving, or stationary to optimize battery usage and location updates. Your location data is end-to-end encrypted and never stored. 33 | NSLocationAlwaysAndWhenInUseUsageDescription 34 | Grid needs access to your location to send updates to your Grid in the background, such as sharing your real-time location with family during a trip or updating group members on your current position. Your location data is end-to-end encrypted and never stored. 35 | NSLocationAlwaysUsageDescription 36 | Grid uses your location to continuously share your location with your selected contacts or groups, even when the app is not open, such as allowing friends to follow your trip in real time. Your location is end-to-end encrypted and never stored. 37 | NSLocationWhenInUseUsageDescription 38 | Grid uses your location to share your current location with your selected contacts or groups, such as letting your family know where you are during an outing. Your location is end-to-end encrypted and never stored. 39 | UIApplicationSupportsIndirectInputEvents 40 | 41 | UIBackgroundModes 42 | 43 | fetch 44 | location 45 | remote-notification 46 | 47 | UILaunchStoryboardName 48 | LaunchScreen 49 | UIMainStoryboardFile 50 | Main 51 | UISupportedInterfaceOrientations 52 | 53 | UIInterfaceOrientationPortrait 54 | 55 | UISupportedInterfaceOrientations~ipad 56 | 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationPortraitUpsideDown 61 | 62 | UIStatusBarHidden 63 | 64 | UIViewControllerBasedStatusBarAppearance 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/blocs/contacts/contacts_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class ContactsEvent extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class LoadContacts extends ContactsEvent {} 9 | 10 | class RefreshContacts extends ContactsEvent {} 11 | 12 | class DeleteContact extends ContactsEvent { 13 | final String userId; 14 | 15 | DeleteContact(this.userId); 16 | 17 | @override 18 | List get props => [userId]; 19 | } 20 | 21 | class SearchContacts extends ContactsEvent { 22 | final String query; 23 | 24 | SearchContacts(this.query); 25 | 26 | @override 27 | List get props => [query]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/blocs/contacts/contacts_state.dart: -------------------------------------------------------------------------------- 1 | // contacts_state.dart 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:grid_frontend/models/contact_display.dart'; 4 | 5 | abstract class ContactsState extends Equatable { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ContactsInitial extends ContactsState {} 11 | 12 | class ContactsLoading extends ContactsState {} 13 | 14 | class ContactsLoaded extends ContactsState { 15 | final List contacts; 16 | 17 | ContactsLoaded(this.contacts); 18 | 19 | @override 20 | List get props => [contacts]; 21 | } 22 | 23 | class ContactsError extends ContactsState { 24 | final String message; 25 | 26 | ContactsError(this.message); 27 | 28 | @override 29 | List get props => [message]; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/blocs/groups/groups_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class GroupsEvent extends Equatable { 4 | const GroupsEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class LoadGroups extends GroupsEvent {} 11 | 12 | class RefreshGroups extends GroupsEvent {} 13 | 14 | class SearchGroups extends GroupsEvent { 15 | final String query; 16 | const SearchGroups(this.query); 17 | 18 | @override 19 | List get props => [query]; 20 | } 21 | 22 | class DeleteGroup extends GroupsEvent { 23 | final String roomId; 24 | const DeleteGroup(this.roomId); 25 | 26 | @override 27 | List get props => [roomId]; 28 | } 29 | 30 | class UpdateGroup extends GroupsEvent { 31 | final String roomId; 32 | const UpdateGroup(this.roomId); 33 | 34 | @override 35 | List get props => [roomId]; 36 | } 37 | 38 | class LoadGroupMembers extends GroupsEvent { 39 | final String roomId; 40 | const LoadGroupMembers(this.roomId); 41 | 42 | @override 43 | List get props => [roomId]; 44 | } 45 | 46 | class UpdateMemberStatus extends GroupsEvent { 47 | final String roomId; 48 | final String userId; 49 | final String status; 50 | 51 | UpdateMemberStatus(this.roomId, this.userId, this.status); 52 | 53 | @override 54 | List get props => [roomId, userId, status]; 55 | } -------------------------------------------------------------------------------- /lib/blocs/groups/groups_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:grid_frontend/models/room.dart'; 3 | import 'package:grid_frontend/models/grid_user.dart'; 4 | 5 | abstract class GroupsState extends Equatable { 6 | const GroupsState(); 7 | 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class GroupsInitial extends GroupsState {} 13 | 14 | class GroupsLoading extends GroupsState {} 15 | 16 | class GroupsError extends GroupsState { 17 | final String message; 18 | const GroupsError(this.message); 19 | 20 | @override 21 | List get props => [message]; 22 | } 23 | 24 | class GroupsLoaded extends GroupsState { 25 | final List groups; 26 | final String? selectedRoomId; 27 | final List? selectedRoomMembers; 28 | final Map? membershipStatuses; 29 | 30 | const GroupsLoaded( 31 | this.groups, { 32 | this.selectedRoomId, 33 | this.selectedRoomMembers, 34 | this.membershipStatuses, 35 | }); 36 | 37 | GroupsLoaded copyWith({ 38 | List? groups, 39 | String? selectedRoomId, 40 | List? selectedRoomMembers, 41 | Map? membershipStatuses, 42 | }) { 43 | return GroupsLoaded( 44 | groups ?? this.groups, 45 | selectedRoomId: selectedRoomId ?? this.selectedRoomId, 46 | selectedRoomMembers: selectedRoomMembers ?? this.selectedRoomMembers, 47 | membershipStatuses: membershipStatuses ?? this.membershipStatuses, 48 | ); 49 | } 50 | 51 | // Create a new instance with cleared member data but keeping groups 52 | GroupsLoaded clearMemberData() { 53 | return GroupsLoaded( 54 | groups, 55 | selectedRoomId: null, 56 | selectedRoomMembers: null, 57 | membershipStatuses: null, 58 | ); 59 | } 60 | 61 | // Check if member data is loaded 62 | bool get hasMemberData => selectedRoomId != null && 63 | selectedRoomMembers != null && 64 | membershipStatuses != null; 65 | 66 | // Get member status safely 67 | String getMemberStatus(String userId) { 68 | return membershipStatuses?[userId] ?? 'join'; 69 | } 70 | 71 | @override 72 | List get props => [ 73 | groups, 74 | if (selectedRoomId != null) selectedRoomId!, 75 | if (selectedRoomMembers != null) selectedRoomMembers!, 76 | if (membershipStatuses != null) membershipStatuses!, 77 | ]; 78 | 79 | @override 80 | String toString() { 81 | return 'GroupsLoaded(groups: ${groups.length}, selectedRoomId: $selectedRoomId, ' 82 | 'memberCount: ${selectedRoomMembers?.length})'; 83 | } 84 | } -------------------------------------------------------------------------------- /lib/blocs/map/map_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:latlong2/latlong.dart'; 5 | import 'package:grid_frontend/blocs/map/map_event.dart'; 6 | import 'package:grid_frontend/blocs/map/map_state.dart'; 7 | import 'package:grid_frontend/repositories/location_repository.dart'; 8 | import 'package:grid_frontend/services/location_manager.dart'; 9 | import 'package:grid_frontend/services/database_service.dart'; 10 | import 'package:grid_frontend/models/user_location.dart'; 11 | 12 | class MapBloc extends Bloc { 13 | final LocationManager locationManager; 14 | final LocationRepository locationRepository; 15 | final DatabaseService databaseService; 16 | late final StreamSubscription _locationSubscription; 17 | final Set _processedLocationIds = {}; 18 | 19 | 20 | MapBloc({ 21 | required this.locationManager, 22 | required this.locationRepository, 23 | required this.databaseService, 24 | }) : super(const MapState()) { 25 | on(_onMapInitialize); 26 | on(_onMapCenterOnUser); 27 | on(_onMapMoveToUser); 28 | on(_onMapLoadUserLocations); 29 | on(_onRemoveUserLocation); 30 | on(_onMapClearSelection); 31 | 32 | _locationSubscription = locationRepository.locationUpdates.listen(_onLocationUpdate); 33 | } 34 | 35 | @override 36 | Future close() { 37 | _locationSubscription.cancel(); 38 | return super.close(); 39 | } 40 | 41 | void _onLocationUpdate(UserLocation location) { 42 | // Create a unique identifier for this location update 43 | final locationId = '${location.userId}:${location.timestamp}'; 44 | 45 | // Skip if we've already processed this exact location 46 | if (_processedLocationIds.contains(locationId)) { 47 | return; 48 | } 49 | 50 | final updatedLocations = List.from(state.userLocations); 51 | // Remove any existing location for this user 52 | updatedLocations.removeWhere((loc) => loc.userId == location.userId); 53 | // Add the new location 54 | updatedLocations.add(location); 55 | 56 | _processedLocationIds.add(locationId); 57 | // Keep set size manageable 58 | if (_processedLocationIds.length > 1000) { 59 | _processedLocationIds.clear(); 60 | } 61 | 62 | emit(state.copyWith(userLocations: updatedLocations)); 63 | } 64 | 65 | Future _onMapInitialize(MapInitialize event, 66 | Emitter emit) async { 67 | // Load initial data if needed, for example: 68 | // Maybe load user locations right away 69 | add(MapLoadUserLocations()); 70 | emit(state.copyWith(isLoading: false)); 71 | } 72 | 73 | void _onRemoveUserLocation(RemoveUserLocation event, Emitter emit) { 74 | print("MapBloc: Removing location for user: ${event.userId}"); 75 | final updatedLocations = state.userLocations 76 | .where((location) => location.userId != event.userId) 77 | .toList(); 78 | print("MapBloc: Locations before: ${state.userLocations.length}, after: ${updatedLocations.length}"); 79 | emit(state.copyWith(userLocations: updatedLocations)); 80 | } 81 | 82 | Future _onMapCenterOnUser(MapCenterOnUser event, 83 | Emitter emit) async { 84 | final currentPosition = locationManager.currentLatLng; 85 | if (currentPosition != null) { 86 | final userLocation = LatLng( 87 | currentPosition.latitude!, currentPosition.longitude!); 88 | emit(state.copyWith(center: userLocation)); 89 | } else { 90 | emit(state.copyWith(error: 'No user location available')); 91 | } 92 | } 93 | 94 | Future _onMapMoveToUser(MapMoveToUser event, Emitter emit) async { 95 | try { 96 | // Add small delay to let any pending updates finish 97 | await Future.delayed(const Duration(milliseconds: 100)); 98 | 99 | final userLocationData = await locationRepository.getLatestLocationFromHistory(event.userId); 100 | 101 | if (userLocationData != null) { 102 | print("New center: ${userLocationData.position}"); 103 | 104 | 105 | 106 | // Force map update with two-step emit 107 | emit(state.copyWith(center: null)); 108 | emit(state.copyWith( 109 | center: userLocationData.position, 110 | moveCount: state.moveCount + 1, 111 | isLoading: false, 112 | selectedUserId: event.userId 113 | )); 114 | } else { 115 | print("Latest location not available for user"); 116 | emit(state.copyWith(error: 'Location not available for this user.')); 117 | } 118 | } catch (e) { 119 | print("Error moving to user: $e"); 120 | emit(state.copyWith(error: 'Error moving to user location: $e')); 121 | } 122 | } 123 | 124 | Future _onMapLoadUserLocations(MapLoadUserLocations event, Emitter emit) async { 125 | try { 126 | final latestLocations = await locationRepository.getAllLatestLocations(); 127 | 128 | // Ensure no duplicates by using a Map keyed by userId 129 | final locationMap = Map.fromEntries( 130 | latestLocations.map((loc) => MapEntry(loc.userId, loc)) 131 | ); 132 | 133 | emit(state.copyWith( 134 | isLoading: false, 135 | userLocations: locationMap.values.toList() 136 | )); 137 | } catch (e) { 138 | emit(state.copyWith(error: 'Error loading user locations: $e')); 139 | } 140 | } 141 | 142 | void _onMapClearSelection(MapClearSelection event, Emitter emit) { 143 | emit(state.copyWith(clearSelectedUserId: true)); 144 | } 145 | } -------------------------------------------------------------------------------- /lib/blocs/map/map_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class MapEvent extends Equatable { 4 | const MapEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class MapInitialize extends MapEvent {} 11 | 12 | class MapCenterOnUser extends MapEvent {} 13 | 14 | class MapMoveToUser extends MapEvent { 15 | final String userId; 16 | const MapMoveToUser(this.userId); 17 | 18 | @override 19 | List get props => [userId]; 20 | } 21 | 22 | class MapLoadUserLocations extends MapEvent {} 23 | 24 | class RemoveUserLocation extends MapEvent { 25 | final String userId; 26 | const RemoveUserLocation(this.userId); 27 | } 28 | 29 | class MapClearSelection extends MapEvent {} 30 | -------------------------------------------------------------------------------- /lib/blocs/map/map_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:latlong2/latlong.dart'; 3 | import 'package:grid_frontend/models/user_location.dart'; 4 | 5 | class MapState extends Equatable { 6 | final bool isLoading; 7 | final LatLng? center; 8 | final double zoom; 9 | final List userLocations; 10 | final String? error; 11 | final int moveCount; 12 | final String? selectedUserId; 13 | 14 | 15 | const MapState({ 16 | this.isLoading = true, 17 | this.center, 18 | this.zoom = 18.0, 19 | this.userLocations = const [], 20 | this.error, 21 | this.moveCount = 0, 22 | this.selectedUserId, 23 | }); 24 | 25 | MapState copyWith({ 26 | bool? isLoading, 27 | LatLng? center, 28 | double? zoom, 29 | List? userLocations, 30 | String? error, 31 | int? moveCount, 32 | String? selectedUserId, 33 | bool clearSelectedUserId = false, 34 | }) { 35 | return MapState( 36 | isLoading: isLoading ?? this.isLoading, 37 | center: center ?? this.center, 38 | zoom: zoom ?? this.zoom, 39 | userLocations: userLocations ?? this.userLocations, 40 | error: error, 41 | moveCount: moveCount ?? this.moveCount, 42 | selectedUserId: clearSelectedUserId ? null : (selectedUserId ?? this.selectedUserId), 43 | ); 44 | } 45 | 46 | @override 47 | List get props => [isLoading, center, zoom, userLocations, error, moveCount, selectedUserId]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/components/modals/notice_continue_modal.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoticeContinueModal extends StatelessWidget { 4 | final String message; 5 | final VoidCallback? onContinue; 6 | final String? title; 7 | final IconData? icon; 8 | 9 | const NoticeContinueModal({ 10 | Key? key, 11 | required this.message, 12 | this.onContinue, 13 | this.title, 14 | this.icon, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final theme = Theme.of(context); 20 | final colorScheme = theme.colorScheme; 21 | 22 | return Dialog( 23 | backgroundColor: Colors.transparent, 24 | child: Container( 25 | constraints: BoxConstraints( 26 | maxWidth: 400, 27 | ), 28 | decoration: BoxDecoration( 29 | color: colorScheme.surface, 30 | borderRadius: BorderRadius.circular(24), 31 | boxShadow: [ 32 | BoxShadow( 33 | color: colorScheme.shadow.withOpacity(0.15), 34 | blurRadius: 20, 35 | offset: const Offset(0, 8), 36 | ), 37 | ], 38 | ), 39 | child: Padding( 40 | padding: const EdgeInsets.all(24.0), 41 | child: Column( 42 | mainAxisSize: MainAxisSize.min, 43 | children: [ 44 | // Icon 45 | Container( 46 | width: 64, 47 | height: 64, 48 | decoration: BoxDecoration( 49 | color: colorScheme.errorContainer, 50 | shape: BoxShape.circle, 51 | ), 52 | child: Icon( 53 | icon ?? Icons.info_outline, 54 | color: colorScheme.error, 55 | size: 32, 56 | ), 57 | ), 58 | const SizedBox(height: 20), 59 | 60 | // Title 61 | if (title != null) ...[ 62 | Text( 63 | title!, 64 | style: theme.textTheme.headlineSmall?.copyWith( 65 | fontWeight: FontWeight.bold, 66 | color: colorScheme.onSurface, 67 | ), 68 | textAlign: TextAlign.center, 69 | ), 70 | const SizedBox(height: 12), 71 | ], 72 | 73 | // Message 74 | Text( 75 | message, 76 | textAlign: TextAlign.center, 77 | style: theme.textTheme.bodyMedium?.copyWith( 78 | color: colorScheme.onSurfaceVariant, 79 | height: 1.5, 80 | ), 81 | ), 82 | const SizedBox(height: 28), 83 | 84 | // Continue Button 85 | SizedBox( 86 | width: double.infinity, 87 | height: 48, 88 | child: ElevatedButton( 89 | style: ElevatedButton.styleFrom( 90 | backgroundColor: colorScheme.primary, 91 | foregroundColor: colorScheme.onPrimary, 92 | elevation: 0, 93 | shadowColor: Colors.transparent, 94 | shape: RoundedRectangleBorder( 95 | borderRadius: BorderRadius.circular(12), 96 | ), 97 | ), 98 | onPressed: () { 99 | Navigator.of(context).pop(); 100 | if (onContinue != null) { 101 | onContinue!(); 102 | } 103 | }, 104 | child: Text( 105 | 'Continue', 106 | style: theme.textTheme.labelLarge?.copyWith( 107 | fontWeight: FontWeight.w600, 108 | color: colorScheme.onPrimary, 109 | ), 110 | ), 111 | ), 112 | ), 113 | ], 114 | ), 115 | ), 116 | ), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/models/contact_display.dart: -------------------------------------------------------------------------------- 1 | class ContactDisplay { 2 | final String userId; 3 | final String displayName; 4 | final String? avatarUrl; 5 | final String lastSeen; 6 | final String? membershipStatus; 7 | 8 | ContactDisplay({ 9 | required this.userId, 10 | required this.displayName, 11 | this.avatarUrl, 12 | required this.lastSeen, 13 | this.membershipStatus, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/models/grid_user.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class GridUser { 4 | final String userId; 5 | final String? displayName; 6 | final String? avatarUrl; 7 | final String lastSeen; 8 | final String? profileStatus; 9 | 10 | GridUser({ 11 | required this.userId, 12 | this.displayName, 13 | this.avatarUrl, 14 | required this.lastSeen, 15 | this.profileStatus, 16 | }); 17 | 18 | /// Factory method to create a `GridUser` from a map 19 | factory GridUser.fromMap(Map map) { 20 | return GridUser( 21 | userId: map['userId'] as String, 22 | displayName: map['displayName'] as String?, 23 | avatarUrl: map['avatarUrl'] as String?, 24 | lastSeen: map['lastSeen'] as String, 25 | profileStatus: map['profileStatus'] as String?, 26 | ); 27 | } 28 | 29 | /// Converts a `GridUser` to a map 30 | Map toMap() { 31 | return { 32 | 'userId': userId, 33 | 'displayName': displayName, 34 | 'avatarUrl': avatarUrl, 35 | 'lastSeen': lastSeen, 36 | 'profileStatus': profileStatus, 37 | }; 38 | } 39 | 40 | /// Converts a `GridUser` to JSON 41 | String toJson() => jsonEncode(toMap()); 42 | 43 | /// Factory method to create a `GridUser` from JSON 44 | factory GridUser.fromJson(String source) => 45 | GridUser.fromMap(jsonDecode(source) as Map); 46 | } 47 | -------------------------------------------------------------------------------- /lib/models/pending_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:matrix/matrix.dart'; 2 | 3 | class PendingMessage { 4 | final String roomId; 5 | final String eventId; 6 | final MatrixEvent event; 7 | final DateTime queuedAt; 8 | 9 | PendingMessage({ 10 | required this.roomId, 11 | required this.eventId, 12 | required this.event, 13 | DateTime? queuedAt, 14 | }) : queuedAt = queuedAt ?? DateTime.now(); 15 | 16 | Map toJson() { 17 | return { 18 | 'roomId': roomId, 19 | 'eventId': eventId, 20 | 'event': event.toJson(), 21 | 'queuedAt': queuedAt.toIso8601String(), 22 | }; 23 | } 24 | 25 | factory PendingMessage.fromJson(Map json) { 26 | return PendingMessage( 27 | roomId: json['roomId'] as String, 28 | eventId: json['eventId'] as String, 29 | event: MatrixEvent.fromJson(json['event'] as Map), 30 | queuedAt: DateTime.parse(json['queuedAt'] as String), 31 | ); 32 | } 33 | 34 | @override 35 | String toString() { 36 | return 'PendingMessage{roomId: $roomId, eventId: $eventId, queuedAt: $queuedAt}'; 37 | } 38 | } -------------------------------------------------------------------------------- /lib/models/room.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class Room { 4 | final String roomId; 5 | final String name; 6 | final bool isGroup; // `false` for direct rooms, `true` for group rooms 7 | final String lastActivity; // ISO 8601 String for the last activity timestamp 8 | final String? avatarUrl; // Optional URL for room/group icon 9 | final List members; // List of user IDs in the room 10 | final int expirationTimestamp; // Timestamp for expiration, 0 if never expires 11 | 12 | Room({ 13 | required this.roomId, 14 | required this.name, 15 | required this.isGroup, 16 | required this.lastActivity, 17 | this.avatarUrl, 18 | required this.members, 19 | required this.expirationTimestamp, 20 | }); 21 | 22 | // Factory constructor to create an instance from a database map 23 | factory Room.fromMap(Map map) { 24 | return Room( 25 | roomId: map['roomId'] as String, 26 | name: map['name'] as String, 27 | isGroup: map['isGroup'] == 1, // SQLite stores boolean as 0 or 1 28 | lastActivity: map['lastActivity'] as String, 29 | avatarUrl: map['avatarUrl'] as String?, 30 | members: (jsonDecode(map['members']) as List).cast(), 31 | expirationTimestamp: map['expirationTimestamp'] as int, 32 | ); 33 | } 34 | 35 | // Method to convert the model to a map for database insertion 36 | Map toMap() { 37 | return { 38 | 'roomId': roomId, 39 | 'name': name, 40 | 'isGroup': isGroup ? 1 : 0, // SQLite uses 0/1 for booleans 41 | 'lastActivity': lastActivity, 42 | 'avatarUrl': avatarUrl, 43 | 'members': jsonEncode(members), 44 | 'expirationTimestamp': expirationTimestamp, 45 | }; 46 | } 47 | 48 | Room copyWith({ 49 | String? roomId, 50 | String? name, 51 | bool? isGroup, 52 | String? lastActivity, 53 | String? avatarUrl, 54 | List? members, 55 | int? expirationTimestamp, 56 | }) { 57 | return Room( 58 | roomId: roomId ?? this.roomId, 59 | name: name ?? this.name, 60 | isGroup: isGroup ?? this.isGroup, 61 | lastActivity: lastActivity ?? this.lastActivity, 62 | avatarUrl: avatarUrl ?? this.avatarUrl, 63 | members: members ?? List.from(this.members), 64 | expirationTimestamp: expirationTimestamp ?? this.expirationTimestamp, 65 | ); 66 | } 67 | 68 | // Method to serialize the room object to JSON 69 | String toJson() => jsonEncode(toMap()); 70 | 71 | // Factory method to deserialize a room object from JSON 72 | factory Room.fromJson(String source) => 73 | Room.fromMap(jsonDecode(source) as Map); 74 | } 75 | -------------------------------------------------------------------------------- /lib/models/sharing_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:grid_frontend/models/sharing_window.dart'; 4 | 5 | class SharingPreferences { 6 | final int? id; 7 | final String targetId; 8 | final String targetType; 9 | final bool activeSharing; 10 | final List? shareWindows; 11 | 12 | SharingPreferences({ 13 | this.id, 14 | required this.targetId, 15 | required this.targetType, 16 | required this.activeSharing, 17 | this.shareWindows, 18 | }); 19 | 20 | // Convert model to a map for database insertion 21 | Map toMap() { 22 | return { 23 | 'id': id, 24 | 'targetId': targetId, 25 | 'targetType': targetType, 26 | 'activeSharing': activeSharing ? 1 : 0, 27 | // Encode the list of windows as JSON (or null if no windows) 28 | 'sharePeriods': shareWindows != null 29 | ? jsonEncode(shareWindows!.map((w) => w.toJson()).toList()) 30 | : null, 31 | }; 32 | } 33 | 34 | 35 | factory SharingPreferences.fromMap(Map map) { 36 | // If 'sharePeriods' is not null, decode it and parse each window 37 | final rawJson = map['sharePeriods'] as String?; 38 | List? windows; 39 | if (rawJson != null && rawJson.isNotEmpty) { 40 | final List decoded = jsonDecode(rawJson); 41 | windows = decoded.map((item) => SharingWindow.fromJson(item)).toList().cast(); 42 | } 43 | 44 | return SharingPreferences( 45 | id: map['id'] as int?, 46 | targetId: map['targetId'] as String, 47 | targetType: map['targetType'] as String, 48 | activeSharing: (map['activeSharing'] as int) == 1, 49 | shareWindows: windows, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/models/sharing_window.dart: -------------------------------------------------------------------------------- 1 | class SharingWindow { 2 | final String label; 3 | final List days; // e.g. 0 for Mon, 1 for Tue, ... 4 | final bool isAllDay; 5 | final String? startTime; // "09:00" 6 | final String? endTime; // "17:00" 7 | final bool isActive; // <--- new field 8 | 9 | SharingWindow({ 10 | required this.label, 11 | required this.days, 12 | required this.isAllDay, 13 | required this.isActive, 14 | this.startTime, 15 | this.endTime, 16 | }); 17 | 18 | Map toJson() => { 19 | 'label': label, 20 | 'days': days, 21 | 'isAllDay': isAllDay, 22 | 'startTime': startTime, 23 | 'endTime': endTime, 24 | 'isActive': isActive, 25 | }; 26 | 27 | factory SharingWindow.fromJson(Map json) => SharingWindow( 28 | label: json['label'] as String, 29 | days: (json['days'] as List).map((e) => e as int).toList(), 30 | isAllDay: json['isAllDay'] as bool, 31 | startTime: json['startTime'] as String?, 32 | endTime: json['endTime'] as String?, 33 | isActive: json['isActive'] == null 34 | ? true 35 | : json['isActive'] as bool, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/models/user_location.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:encrypt/encrypt.dart' as encrypt; 3 | import 'package:latlong2/latlong.dart'; 4 | import 'package:grid_frontend/utilities/encryption_utils.dart'; 5 | 6 | class UserLocation { 7 | final String userId; 8 | final double latitude; 9 | final double longitude; 10 | final String timestamp; // ISO 8601 format 11 | final String iv; 12 | 13 | UserLocation({ 14 | required this.userId, 15 | required this.latitude, 16 | required this.longitude, 17 | required this.timestamp, 18 | required this.iv, 19 | }); 20 | 21 | // Getter to provide LatLng object for map display 22 | LatLng get position => LatLng(latitude, longitude); 23 | 24 | /// Factory constructor to create an instance from a database map. 25 | /// Handles decryption for latitude and longitude. 26 | factory UserLocation.fromMap(Map map, String encryptionKey) { 27 | final ivString = map['iv'] as String; 28 | 29 | // Decrypt latitude and longitude 30 | final decryptedLatitude = decryptText(map['latitude'] as String, encryptionKey, ivString); 31 | final decryptedLongitude = decryptText(map['longitude'] as String, encryptionKey, ivString); 32 | 33 | return UserLocation( 34 | userId: map['userId'] as String, 35 | latitude: double.parse(decryptedLatitude), 36 | longitude: double.parse(decryptedLongitude), 37 | timestamp: map['timestamp'] as String, 38 | iv: ivString, 39 | ); 40 | } 41 | 42 | /// Convert the model to a map for database insertion. 43 | /// Handles encryption for sensitive fields. 44 | Map toMap(String encryptionKey) { 45 | final ivObject = encrypt.IV.fromBase64(iv); 46 | 47 | // Encrypt latitude and longitude 48 | final encryptedLatitude = encryptText(latitude.toString(), encryptionKey, ivObject); 49 | final encryptedLongitude = encryptText(longitude.toString(), encryptionKey, ivObject); 50 | 51 | return { 52 | 'userId': userId, 53 | 'latitude': encryptedLatitude, 54 | 'longitude': encryptedLongitude, 55 | 'timestamp': timestamp, 56 | 'iv': iv, 57 | }; 58 | } 59 | 60 | /// Serialize to JSON for API or external usage 61 | String toJson(String encryptionKey) => jsonEncode(toMap(encryptionKey)); 62 | 63 | /// Deserialize from JSON 64 | factory UserLocation.fromJson(String source, String encryptionKey) { 65 | final map = jsonDecode(source) as Map; 66 | return UserLocation.fromMap(map, encryptionKey); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/providers/contacts_refresh_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContactsRefreshProvider extends ChangeNotifier { 4 | void refreshContacts() { 5 | notifyListeners(); 6 | } 7 | } -------------------------------------------------------------------------------- /lib/providers/selected_subscreen_provider.dart: -------------------------------------------------------------------------------- 1 | // lib/providers/selected_subscreen_provider.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class SelectedSubscreenProvider with ChangeNotifier { 6 | String _selectedSubscreen = 'contacts'; // Default subscreen 7 | 8 | String get selectedSubscreen => _selectedSubscreen; 9 | 10 | void setSelectedSubscreen(String subscreen) { 11 | _selectedSubscreen = subscreen; 12 | notifyListeners(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/providers/selected_user_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:latlong2/latlong.dart'; 4 | import 'package:grid_frontend/blocs/map/map_bloc.dart'; 5 | import 'package:grid_frontend/blocs/map/map_event.dart'; 6 | 7 | class SelectedUserProvider with ChangeNotifier { 8 | String? _selectedUserId; 9 | 10 | String? get selectedUserId => _selectedUserId; 11 | 12 | void setSelectedUserId(String? userId, BuildContext context) { 13 | print("Selected user: $userId"); 14 | _selectedUserId = userId; 15 | 16 | // Trigger map navigation via MapBloc 17 | if (userId != null) { 18 | context.read().add(MapMoveToUser(userId)); 19 | } 20 | notifyListeners(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/providers/user_location_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:grid_frontend/models/user_location.dart'; 3 | import 'package:grid_frontend/repositories/location_repository.dart'; 4 | import 'package:grid_frontend/repositories/user_repository.dart'; 5 | 6 | class UserLocationProvider with ChangeNotifier { 7 | final Map _userLocations = {}; 8 | final LocationRepository locationRepository; 9 | final UserRepository userRepository; 10 | 11 | UserLocationProvider(this.locationRepository, this.userRepository) { 12 | _initializeLocations(); 13 | _listenForDatabaseUpdates(); 14 | WidgetsBinding.instance.addPostFrameCallback((_) { 15 | notifyListeners(); 16 | }); 17 | 18 | } 19 | 20 | 21 | Future _initializeLocations() async { 22 | final locations = await locationRepository.getAllLatestLocations(); // Use getAllLatestLocations instead 23 | for (var location in locations) { 24 | _userLocations[location.userId] = location; 25 | } 26 | notifyListeners(); 27 | } 28 | 29 | // Modify getLastSeen to return the most recent timestamp 30 | String? getLastSeen(String userId) { 31 | final location = _userLocations[userId]; 32 | if (location == null || location.timestamp == null) return null; 33 | 34 | // Ensure the timestamp is valid 35 | try { 36 | DateTime.parse(location.timestamp!); 37 | return location.timestamp; 38 | } catch (e) { 39 | print("Invalid timestamp for user $userId: ${location.timestamp}"); 40 | return null; 41 | } 42 | } 43 | 44 | void _listenForDatabaseUpdates() { 45 | locationRepository.locationUpdates.listen((location) async { 46 | try { 47 | // Check if user exists in any rooms before updating location 48 | final userRooms = await userRepository.getUserRooms(location.userId); 49 | final directRoom = await userRepository.getDirectRoomForContact(location.userId); 50 | 51 | if (userRooms.isNotEmpty || directRoom != null) { 52 | _userLocations[location.userId] = location; 53 | notifyListeners(); 54 | } else { 55 | // If user doesn't exist in any rooms, remove from cache 56 | _userLocations.remove(location.userId); 57 | notifyListeners(); 58 | } 59 | } catch (e) { 60 | print("Error in location update listener: $e"); 61 | } 62 | }); 63 | } 64 | 65 | List getAllUserLocations() => _userLocations.values.toList(); 66 | 67 | UserLocation? getUserLocation(String userId) { 68 | return _userLocations[userId]; 69 | } 70 | 71 | void updateUserLocation(UserLocation location) { 72 | _userLocations[location.userId] = location; 73 | notifyListeners(); 74 | } 75 | 76 | void debugUserLocations() { 77 | print("DEBUG _userLocations: ${_userLocations.keys.toList()}"); 78 | } 79 | 80 | void removeUserLocation(String userId) { 81 | _userLocations.remove(userId); 82 | notifyListeners(); 83 | } 84 | 85 | void clearAllLocations() { 86 | _userLocations.clear(); 87 | notifyListeners(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/repositories/location_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:sqflite/sqflite.dart'; 3 | import 'package:grid_frontend/models/user_location.dart'; 4 | import 'package:grid_frontend/services/database_service.dart'; 5 | 6 | class LocationRepository { 7 | final DatabaseService _databaseService; 8 | final StreamController _locationUpdatesController = StreamController.broadcast(); 9 | 10 | LocationRepository(this._databaseService); 11 | 12 | static Future createTable(Database db) async { 13 | await db.execute(''' 14 | CREATE TABLE UserLocations ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | userId TEXT, 17 | latitude TEXT, 18 | longitude TEXT, 19 | timestamp TEXT, 20 | iv TEXT, 21 | FOREIGN KEY (userId) REFERENCES Users (id), 22 | UNIQUE(userId) ON CONFLICT REPLACE 23 | ); 24 | '''); 25 | } 26 | /// Stream of location updates, emits a UserLocation whenever one is inserted or updated 27 | Stream get locationUpdates => _locationUpdatesController.stream; 28 | 29 | /// Insert or update a user's location and notify listeners 30 | Future insertLocation(UserLocation location) async { 31 | final db = await _databaseService.database; 32 | final encryptionKey = await _databaseService.getEncryptionKey(); 33 | await db.insert( 34 | 'UserLocations', 35 | location.toMap(encryptionKey), 36 | conflictAlgorithm: ConflictAlgorithm.replace, 37 | ); 38 | // Notify that a location was updated 39 | _locationUpdatesController.add(location); 40 | } 41 | 42 | /// Delete all location data for a specific user 43 | Future deleteUserLocations(String userId) async { 44 | print("Deleting location data for user: $userId"); 45 | final db = await _databaseService.database; 46 | 47 | await db.delete( 48 | 'UserLocations', 49 | where: 'userId = ?', 50 | whereArgs: [userId], 51 | ); 52 | 53 | print("Deleted all location data for user: $userId"); 54 | } 55 | 56 | /// Delete location data for a user, but only if they're not in any other rooms 57 | // In LocationRepository 58 | Future deleteUserLocationsIfNotInRooms(String userId) async { 59 | print("Checking if we should delete location data for user: $userId"); 60 | final db = await _databaseService.database; 61 | 62 | final otherRooms = await db.query( 63 | 'UserRelationships', 64 | where: 'userId = ?', 65 | whereArgs: [userId], 66 | ); 67 | 68 | if (otherRooms.isEmpty) { 69 | print("User $userId not in any rooms, deleting location data"); 70 | await db.delete( 71 | 'UserLocations', 72 | where: 'userId = ?', 73 | whereArgs: [userId], 74 | ); 75 | print("Deleted all location data for user: $userId"); 76 | return true; // Return true if we deleted 77 | } else { 78 | print("User $userId still in ${otherRooms.length} rooms, keeping location data"); 79 | return false; // Return false if we kept the data 80 | } 81 | } 82 | 83 | /// Fetch the latest location for a given user 84 | Future getLatestLocation(String userId) async { 85 | final db = await _databaseService.database; 86 | final encryptionKey = await _databaseService.getEncryptionKey(); 87 | final results = await db.query( 88 | 'UserLocations', 89 | where: 'userId = ?', 90 | whereArgs: [userId], 91 | orderBy: 'timestamp DESC', 92 | limit: 1, 93 | ); 94 | if (results.isNotEmpty) { 95 | return UserLocation.fromMap(results.first, encryptionKey); 96 | } 97 | return null; 98 | } 99 | 100 | Future getLatestLocationFromHistory(String userId) async { 101 | final db = await _databaseService.database; 102 | final encryptionKey = await _databaseService.getEncryptionKey(); 103 | 104 | // Modified query to handle ISO8601 timestamps correctly 105 | final results = await db.rawQuery(''' 106 | SELECT * FROM UserLocations 107 | WHERE userId = ? 108 | ORDER BY strftime('%s', timestamp) DESC 109 | LIMIT 1 110 | ''', [userId]); 111 | 112 | if (results.isNotEmpty) { 113 | return UserLocation.fromMap(results.first, encryptionKey); 114 | } 115 | return null; 116 | } 117 | 118 | Future> getAllLatestLocations() async { 119 | final db = await _databaseService.database; 120 | final encryptionKey = await _databaseService.getEncryptionKey(); 121 | 122 | // Modified query to handle ISO8601 timestamps correctly 123 | final results = await db.rawQuery(''' 124 | SELECT l.* FROM UserLocations l 125 | INNER JOIN ( 126 | SELECT userId, MAX(strftime('%s', timestamp)) as maxTime 127 | FROM UserLocations 128 | GROUP BY userId 129 | ) latest ON l.userId = latest.userId 130 | AND strftime('%s', l.timestamp) = latest.maxTime 131 | '''); 132 | 133 | return results.map((row) => UserLocation.fromMap(row, encryptionKey)).toList(); 134 | } 135 | 136 | 137 | /// Fetch all locations for all users 138 | Future> getAllLocations() async { 139 | final db = await _databaseService.database; 140 | final encryptionKey = await _databaseService.getEncryptionKey(); 141 | final results = await db.query( 142 | 'UserLocations', 143 | orderBy: 'timestamp DESC', 144 | ); 145 | return results.map((row) => UserLocation.fromMap(row, encryptionKey)).toList(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/repositories/sharing_preferences_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import 'package:grid_frontend/models/sharing_preferences.dart'; 3 | import 'package:grid_frontend/services/database_service.dart'; 4 | 5 | class SharingPreferencesRepository { 6 | final DatabaseService _databaseService; 7 | 8 | SharingPreferencesRepository(this._databaseService); 9 | 10 | /// Create the `SharingPreferences` table 11 | static Future createTable(Database db) async { 12 | await db.execute(''' 13 | CREATE TABLE SharingPreferences ( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | targetId TEXT NOT NULL, 16 | targetType TEXT NOT NULL, 17 | activeSharing INTEGER DEFAULT 1, 18 | sharePeriods TEXT, 19 | UNIQUE(targetId, targetType) -- Ensures no duplicate preferences for the same target 20 | ); 21 | '''); 22 | } 23 | 24 | /// Insert or update sharing preferences 25 | Future setSharingPreferences(SharingPreferences preferences) async { 26 | final db = await _databaseService.database; 27 | await db.insert( 28 | 'SharingPreferences', 29 | preferences.toMap(), 30 | conflictAlgorithm: ConflictAlgorithm.replace, // Replace if exists 31 | ); 32 | } 33 | 34 | /// Fetch sharing preferences for a specific target 35 | Future getSharingPreferences(String targetId, String targetType) async { 36 | final db = await _databaseService.database; 37 | final results = await db.query( 38 | 'SharingPreferences', 39 | where: 'targetId = ? AND targetType = ?', 40 | whereArgs: [targetId, targetType], 41 | ); 42 | if (results.isNotEmpty) { 43 | return SharingPreferences.fromMap(results.first); 44 | } 45 | return null; // Return null if no results 46 | } 47 | 48 | /// Fetch all sharing preferences 49 | Future> getAllSharingPreferences() async { 50 | final db = await _databaseService.database; 51 | final results = await db.query('SharingPreferences'); 52 | return results.map((result) => SharingPreferences.fromMap(result)).toList(); 53 | } 54 | 55 | /// Delete sharing preferences for a specific target 56 | Future deleteSharingPreferences(String targetId, String targetType) async { 57 | final db = await _databaseService.database; 58 | await db.delete( 59 | 'SharingPreferences', 60 | where: 'targetId = ? AND targetType = ?', 61 | whereArgs: [targetId, targetType], 62 | ); 63 | } 64 | 65 | /// Clear all sharing preferences (used for resets or testing) 66 | Future clearAllSharingPreferences() async { 67 | final db = await _databaseService.database; 68 | await db.delete('SharingPreferences'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/repositories/user_keys_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import 'package:grid_frontend/services/database_service.dart'; 3 | 4 | class UserKeysRepository { 5 | final DatabaseService _databaseService; 6 | 7 | UserKeysRepository(this._databaseService); 8 | 9 | /// Create the UserKeys table 10 | static Future createTable(Database db) async { 11 | await db.execute(''' 12 | CREATE TABLE UserKeys ( 13 | userId TEXT PRIMARY KEY, 14 | curve25519Key TEXT NOT NULL, 15 | ed25519Key TEXT NOT NULL, 16 | approvedKeys TEXT DEFAULT 'false' 17 | ); 18 | '''); 19 | } 20 | 21 | 22 | /// Insert or update user keys 23 | Future upsertKeys(String userId, String curve25519Key, String ed25519Key) async { 24 | final db = await _databaseService.database; 25 | await db.insert( 26 | 'UserKeys', 27 | { 28 | 'userId': userId, 29 | 'curve25519Key': curve25519Key, 30 | 'ed25519Key': ed25519Key, 31 | }, 32 | conflictAlgorithm: ConflictAlgorithm.replace, 33 | ); 34 | } 35 | 36 | /// Get keys for a specific user 37 | Future?> getKeysByUserId(String userId) async { 38 | final db = await _databaseService.database; 39 | final results = await db.query( 40 | 'UserKeys', 41 | where: 'userId = ?', 42 | whereArgs: [userId], 43 | ); 44 | if (results.isNotEmpty) { 45 | return { 46 | 'curve25519Key': results.first['curve25519Key'] as String, 47 | 'ed25519Key': results.first['ed25519Key'] as String, 48 | }; 49 | } 50 | return null; 51 | } 52 | 53 | /// Get all user keys 54 | Future>> getAllKeys() async { 55 | final db = await _databaseService.database; 56 | final results = await db.query('UserKeys'); 57 | return results.map((row) { 58 | return { 59 | 'userId': row['userId'] as String, 60 | 'curve25519Key': row['curve25519Key'] as String, 61 | 'ed25519Key': row['ed25519Key'] as String, 62 | }; 63 | }).toList(); 64 | } 65 | 66 | /// Delete keys for a specific user 67 | Future deleteKeysByUserId(String userId) async { 68 | final db = await _databaseService.database; 69 | await db.delete('UserKeys', where: 'userId = ?', whereArgs: [userId]); 70 | } 71 | 72 | /// Delete all keys 73 | Future deleteAllKeys() async { 74 | final db = await _databaseService.database; 75 | await db.delete('UserKeys'); 76 | } 77 | 78 | /// Get the approval status of keys for a specific user 79 | Future getApprovedKeys(String userId) async { 80 | final db = await _databaseService.database; 81 | final result = await db.query( 82 | 'UserKeys', 83 | columns: ['approvedKeys'], 84 | where: 'userId = ?', 85 | whereArgs: [userId], 86 | ); 87 | if (result.isNotEmpty) { 88 | return result.first['approvedKeys']?.toString().toLowerCase() == 'true'; 89 | } 90 | return null; // Returns null if no record is found for userId 91 | } 92 | 93 | Future updateApprovedKeys(String userId, bool approvedKeys) async { 94 | final db = await _databaseService.database; 95 | 96 | // Update all device keys for the given user to the new approval status 97 | await db.update( 98 | 'UserKeys', // Assuming the table is called 'UserKeys' 99 | {'approvedKeys': approvedKeys.toString()}, // Set the approval status 100 | where: 'userId = ?', // Ensure it targets only the specified user's keys 101 | whereArgs: [userId], 102 | ); 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /lib/screens/onboarding/username_select_screen.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/lib/screens/onboarding/username_select_screen.dart -------------------------------------------------------------------------------- /lib/screens/settings/notifications_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class NotificationSettingsPage extends StatefulWidget { 5 | @override 6 | _NotificationSettingsPageState createState() => _NotificationSettingsPageState(); 7 | } 8 | 9 | class _NotificationSettingsPageState extends State { 10 | bool _showName = true; 11 | bool _showAlerts = true; 12 | bool _showActions = true; 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | _loadPreferences(); 18 | } 19 | 20 | Future _loadPreferences() async { 21 | final prefs = await SharedPreferences.getInstance(); 22 | setState(() { 23 | _showName = prefs.getBool('showName') ?? true; 24 | _showAlerts = prefs.getBool('showAlerts') ?? true; 25 | _showActions = prefs.getBool('showActions') ?? true; 26 | }); 27 | } 28 | 29 | Future _savePreferences() async { 30 | final prefs = await SharedPreferences.getInstance(); 31 | await prefs.setBool('showName', _showName); 32 | await prefs.setBool('showAlerts', _showAlerts); 33 | await prefs.setBool('showActions', _showActions); 34 | ScaffoldMessenger.of(context).showSnackBar( 35 | SnackBar(content: Text('Settings saved')), 36 | ); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | final theme = Theme.of(context); 42 | final colorScheme = theme.colorScheme; 43 | 44 | return Scaffold( 45 | appBar: AppBar( 46 | title: Text('Notification Settings'), 47 | backgroundColor: colorScheme.background, 48 | leading: BackButton(color: colorScheme.onBackground), 49 | ), 50 | body: Container( 51 | padding: EdgeInsets.all(20), 52 | color: colorScheme.background, 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | children: [ 56 | CheckboxListTile( 57 | title: Text( 58 | 'Show Name', 59 | style: TextStyle(color: colorScheme.onBackground), 60 | ), 61 | value: _showName, 62 | onChanged: (bool? value) { 63 | setState(() { 64 | _showName = value ?? false; 65 | }); 66 | }, 67 | activeColor: colorScheme.primary, 68 | ), 69 | CheckboxListTile( 70 | title: Text( 71 | 'Show Alerts', 72 | style: TextStyle(color: colorScheme.onBackground), 73 | ), 74 | value: _showAlerts, 75 | onChanged: (bool? value) { 76 | setState(() { 77 | _showAlerts = value ?? false; 78 | }); 79 | }, 80 | activeColor: colorScheme.primary, 81 | ), 82 | CheckboxListTile( 83 | title: Text( 84 | 'Show Actions', 85 | style: TextStyle(color: colorScheme.onBackground), 86 | ), 87 | value: _showActions, 88 | onChanged: (bool? value) { 89 | setState(() { 90 | _showActions = value ?? false; 91 | }); 92 | }, 93 | activeColor: colorScheme.primary, 94 | ), 95 | Spacer(), 96 | Center( 97 | child: ElevatedButton( 98 | onPressed: _savePreferences, 99 | child: Text('Save'), 100 | style: ElevatedButton.styleFrom( 101 | backgroundColor: colorScheme.primary, // Corrected this line 102 | foregroundColor: colorScheme.onPrimary, // Corrected this line 103 | ), 104 | ), 105 | ), 106 | ], 107 | ), 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/services/android_background_task.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:path_provider/path_provider.dart'; 4 | import 'package:matrix/matrix.dart'; 5 | import 'package:grid_frontend/services/database_service.dart'; 6 | import 'package:grid_frontend/repositories/location_repository.dart'; 7 | import 'package:grid_frontend/repositories/user_repository.dart'; 8 | import 'package:grid_frontend/repositories/room_repository.dart'; 9 | import 'package:grid_frontend/repositories/sharing_preferences_repository.dart'; 10 | import 'package:grid_frontend/repositories/user_keys_repository.dart'; 11 | import 'package:grid_frontend/services/user_service.dart'; 12 | 13 | @pragma('vm:entry-point') 14 | void headlessTask(bg.HeadlessEvent headlessEvent) async { 15 | print('[BackgroundGeolocation HeadlessTask]: $headlessEvent'); 16 | 17 | switch (headlessEvent.name) { 18 | case bg.Event.LOCATION: 19 | if (headlessEvent.event is bg.Location) { 20 | bg.Location location = headlessEvent.event as bg.Location; 21 | print('- Location: $location'); 22 | await processBackgroundLocation(location); 23 | } 24 | break; 25 | 26 | case bg.Event.HEARTBEAT: 27 | if (headlessEvent.event is bg.HeartbeatEvent) { 28 | final bg.HeartbeatEvent hbEvent = headlessEvent.event as bg.HeartbeatEvent; 29 | final bg.Location? location = hbEvent.location; 30 | 31 | print('- Heartbeat location: $location'); 32 | await processBackgroundLocation(location!); 33 | } 34 | break; 35 | 36 | 37 | 38 | } 39 | } 40 | 41 | Future processBackgroundLocation(bg.Location location) async { 42 | Client? client; 43 | HiveCollectionsDatabase? db; 44 | 45 | try { 46 | // Initialize database 47 | final databaseService = DatabaseService(); 48 | await databaseService.initDatabase(); 49 | 50 | // Initialize Matrix client 51 | client = Client( 52 | 'Grid App', 53 | databaseBuilder: (_) async { 54 | final dir = await getApplicationSupportDirectory(); 55 | db = HiveCollectionsDatabase('grid_app', dir.path); 56 | await db?.open(); 57 | return db!; 58 | }, 59 | ); 60 | await client.init(); 61 | client.backgroundSync = false; 62 | 63 | 64 | // Initialize repositories 65 | final locationRepository = LocationRepository(databaseService); 66 | final userRepository = UserRepository(databaseService); 67 | final sharingPreferencesRepository = SharingPreferencesRepository(databaseService); 68 | final userKeysRepository = UserKeysRepository(databaseService); 69 | final roomRepository = RoomRepository(databaseService); 70 | 71 | // Initialize services 72 | final userService = UserService( 73 | client, 74 | locationRepository, 75 | sharingPreferencesRepository, 76 | ); 77 | 78 | // Process rooms and send updates 79 | List rooms = client.rooms; 80 | print("Grid: Found ${rooms.length} total rooms to process"); 81 | 82 | final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; 83 | 84 | for (Room room in rooms) { 85 | try { 86 | print("Grid: Processing room ${room.name} (${room.id})"); 87 | 88 | if (!_shouldProcessRoom(room, currentTimestamp)) continue; 89 | 90 | var joinedMembers = room 91 | .getParticipants() 92 | .where((member) => member.membership == Membership.join) 93 | .toList(); 94 | print("Grid: Room has ${joinedMembers.length} joined members"); 95 | 96 | if (!joinedMembers.any((member) => member.id == client?.userID)) { 97 | print("Grid: Skipping room ${room.id} - I am not a joined member"); 98 | continue; 99 | } 100 | 101 | if (joinedMembers.length > 1) { 102 | if (!await _checkSharingWindow(room, joinedMembers, client, userService)) continue; 103 | 104 | await _sendLocationUpdate(room, location); 105 | } else { 106 | print("Grid: Skipping room ${room.id} - insufficient members"); 107 | } 108 | } catch (e) { 109 | print('Error processing room ${room.name}: $e'); 110 | continue; 111 | } 112 | } 113 | 114 | // Important: Wait for any pending operations to complete 115 | await client?.dispose(closeDatabase: true); 116 | 117 | } catch (e) { 118 | print('[Background Task Error]: $e'); 119 | } finally { 120 | try { 121 | // Close the database after all operations are done 122 | await client?.dispose(); 123 | } catch (e) { 124 | print('Error during cleanup: $e'); 125 | } 126 | } 127 | } 128 | 129 | bool _shouldProcessRoom(Room room, int currentTimestamp) { 130 | if (!room.name.startsWith('Grid:')) { 131 | print("Grid: Skipping non-Grid room: ${room.name}"); 132 | return false; 133 | } 134 | 135 | if (room.name.startsWith('Grid:Group:')) { 136 | final parts = room.name.split(':'); 137 | if (parts.length < 3) return false; 138 | 139 | final expirationStr = parts[2]; 140 | final expirationTimestamp = int.tryParse(expirationStr); 141 | print("Grid: Group room expiration: $expirationTimestamp, current: $currentTimestamp"); 142 | 143 | if (expirationTimestamp != null && 144 | expirationTimestamp != 0 && 145 | expirationTimestamp < currentTimestamp) { 146 | print("Grid: Skipping expired group room"); 147 | return false; 148 | } 149 | } else if (!room.name.startsWith('Grid:Direct:')) { 150 | print("Grid: Skipping unknown Grid room type: ${room.name}"); 151 | return false; 152 | } 153 | 154 | return true; 155 | } 156 | 157 | Future _checkSharingWindow(Room room, List joinedMembers, Client client, UserService userService) async { 158 | if (joinedMembers.length == 2 && room.name.startsWith('Grid:Direct:')) { 159 | var otherUsers = joinedMembers.where((member) => member.id != client.userID); 160 | var otherUser = otherUsers.first.id; 161 | final isSharing = await userService.isInSharingWindow(otherUser); 162 | if (!isSharing) { 163 | print("Grid: Skipping direct room ${room.id} - not in sharing window with $otherUser"); 164 | return false; 165 | } 166 | print("In sharing window"); 167 | } 168 | 169 | if (joinedMembers.length >= 2 && room.name.startsWith('Grid:Group:')) { 170 | final isSharing = await userService.isGroupInSharingWindow(room.id); 171 | if (!isSharing) { 172 | print("Grid: Skipping group room ${room.id} - not in sharing window"); 173 | return false; 174 | } 175 | print("In sharing window"); 176 | } 177 | 178 | return true; 179 | } 180 | 181 | Future _sendLocationUpdate(Room room, bg.Location location) async { 182 | final eventContent = { 183 | 'msgtype': 'm.location', 184 | 'body': 'Current location', 185 | 'geo_uri': 'geo:${location.coords.latitude},${location.coords.longitude}', 186 | 'description': 'Current location', 187 | 'timestamp': DateTime.now().toUtc().toIso8601String(), 188 | }; 189 | 190 | await room.sendEvent(eventContent); 191 | print("Grid: Location event sent to room ${room.id} / ${room.name}"); 192 | } -------------------------------------------------------------------------------- /lib/services/backwards_compatibility_service.dart: -------------------------------------------------------------------------------- 1 | // lib/services/backwards_compatibility_service.dart 2 | 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | import 'package:grid_frontend/repositories/user_repository.dart'; 5 | import 'package:grid_frontend/repositories/sharing_preferences_repository.dart'; 6 | import 'package:grid_frontend/models/sharing_preferences.dart'; 7 | 8 | class BackwardsCompatibilityService { 9 | final UserRepository _userRepository; 10 | final SharingPreferencesRepository _sharingPrefsRepo; 11 | 12 | BackwardsCompatibilityService( 13 | this._userRepository, 14 | this._sharingPrefsRepo, 15 | ); 16 | 17 | /// Run any "backfill" or "fixup" routines that only need to happen once 18 | Future runBackfillIfNeeded() async { 19 | final prefs = await SharedPreferences.getInstance(); 20 | final alreadyDone = prefs.getBool('hasBackfillSharingPrefs') ?? false; 21 | 22 | if (alreadyDone) { 23 | // Already ran once—no need to do it again. 24 | return; 25 | } 26 | 27 | // 1. Fetch all direct contacts 28 | final allDirectContacts = await _userRepository.getDirectContacts(); 29 | 30 | // 2. For each contact, check if there's a SharingPreferences row 31 | for (final contact in allDirectContacts) { 32 | final contactId = contact.userId; 33 | final existingPrefs = 34 | await _sharingPrefsRepo.getSharingPreferences(contactId, 'user'); 35 | 36 | if (existingPrefs == null) { 37 | // 3. Insert a default row 38 | final defaultPrefs = SharingPreferences( 39 | targetId: contactId, 40 | targetType: 'user', 41 | activeSharing: true, 42 | shareWindows: [], 43 | ); 44 | await _sharingPrefsRepo.setSharingPreferences(defaultPrefs); 45 | print("Created default sharing prefs for $contactId"); 46 | } 47 | } 48 | 49 | await prefs.setBool('hasBackfillSharingPrefs', true); 50 | print("Backfill of sharing preferences complete."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/services/database_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import 'package:path/path.dart'; 3 | import 'package:path_provider/path_provider.dart'; 4 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 5 | import 'package:encrypt/encrypt.dart'; 6 | import 'package:grid_frontend/repositories/location_repository.dart'; 7 | import 'package:grid_frontend/repositories/room_repository.dart'; 8 | import 'package:grid_frontend/repositories/user_repository.dart'; 9 | import 'package:grid_frontend/repositories/sharing_preferences_repository.dart'; 10 | import 'package:grid_frontend/repositories/user_keys_repository.dart'; 11 | 12 | class DatabaseService { 13 | static Database? _database; 14 | final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); 15 | 16 | /// Get the database instance (Singleton) 17 | Future get database async { 18 | if (_database != null) return _database!; 19 | _database = await initDatabase(); 20 | return _database!; 21 | } 22 | 23 | /// Initialize the database 24 | Future initDatabase() async { 25 | var directory = await getApplicationDocumentsDirectory(); 26 | String path = join(directory.path, 'secure_grid.db'); 27 | 28 | return await openDatabase( 29 | path, 30 | version: 1, // Reset to version 1 since we don't need migrations 31 | onCreate: (db, version) async { 32 | await _initializeEncryptionKey(); 33 | await UserRepository.createTables(db); 34 | await RoomRepository.createTables(db); 35 | await LocationRepository.createTable(db); 36 | await SharingPreferencesRepository.createTable(db); 37 | await UserKeysRepository.createTable(db); 38 | }, 39 | ); 40 | } 41 | 42 | /// Ensures an encryption key exists in secure storage 43 | Future _initializeEncryptionKey() async { 44 | String? key = await _secureStorage.read(key: 'encryptionKey'); 45 | if (key == null) { 46 | final keyBytes = Key.fromSecureRandom(32); 47 | key = keyBytes.base64; 48 | await _secureStorage.write(key: 'encryptionKey', value: key); 49 | print('Generated new encryption key.'); 50 | } else { 51 | print('Encryption key exists.'); 52 | } 53 | } 54 | 55 | /// Fetch the encryption key 56 | Future getEncryptionKey() async { 57 | String? key = await _secureStorage.read(key: 'encryptionKey'); 58 | if (key == null) { 59 | throw Exception('Encryption key not found!'); 60 | } 61 | return key; 62 | } 63 | 64 | /// Clear all data from the database 65 | Future clearAllData() async { 66 | final db = await database; 67 | final tables = ['Users', 'UserLocations', 'Rooms', 'SharingPreferences', 'UserKeys']; 68 | for (final table in tables) { 69 | await db.delete(table); 70 | } 71 | } 72 | 73 | /// Delete and reinitialize the database 74 | Future deleteAndReinitialize() async { 75 | print("Deleting database..."); 76 | final dbPath = await getDatabasesPath(); 77 | String path = join(dbPath, 'secure_grid.db'); 78 | 79 | await deleteDatabase(path); 80 | _database = await initDatabase(); 81 | print("Re-initialized db"); 82 | } 83 | } -------------------------------------------------------------------------------- /lib/services/matrix_service.dart: -------------------------------------------------------------------------------- 1 | // lib/services/matrix_service.dart 2 | 3 | import 'package:matrix/matrix.dart'; 4 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 5 | 6 | class MatrixService { 7 | final Client client; 8 | final FlutterSecureStorage secureStorage; 9 | 10 | MatrixService(String homeserver) 11 | : client = Client(homeserver), 12 | secureStorage = FlutterSecureStorage(); 13 | 14 | Future login(String username, String password) async { 15 | try { 16 | await client.login( 17 | LoginType.mLoginPassword, 18 | identifier: AuthenticationUserIdentifier(user: username), 19 | password: password, 20 | ); 21 | await secureStorage.write(key: 'access_token', value: client.accessToken); 22 | } catch (e) { 23 | rethrow; 24 | } 25 | } 26 | 27 | Future logout() async { 28 | await client.logout(); 29 | await secureStorage.delete(key: 'access_token'); 30 | } 31 | 32 | Future restoreSession() async { 33 | final accessToken = await secureStorage.read(key: 'access_token'); 34 | if (accessToken != null) { 35 | client.accessToken = accessToken; 36 | await client.sync(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/services/message_processor.dart: -------------------------------------------------------------------------------- 1 | import 'package:grid_frontend/repositories/location_repository.dart'; 2 | import 'package:grid_frontend/utilities/message_parser.dart'; 3 | import 'package:grid_frontend/models/user_location.dart'; 4 | import 'package:matrix/encryption/encryption.dart'; 5 | import 'package:matrix/matrix.dart'; 6 | 7 | class MessageProcessor { 8 | final Client client; 9 | final Encryption encryption; 10 | final LocationRepository locationRepository; 11 | final MessageParser messageParser; 12 | 13 | MessageProcessor( 14 | this.locationRepository, 15 | this.messageParser, 16 | this.client, 17 | ) : encryption = Encryption(client: client); 18 | 19 | /// Process a single event from a room. Decrypt if necessary, 20 | /// then parse and store location messages if found. 21 | /// Returns a Map representing the message if it's a `m.room.message`, 22 | /// or null otherwise. 23 | Future?> processEvent(String roomId, MatrixEvent matrixEvent) async { 24 | final room = client.getRoomById(roomId); 25 | if (room == null) { 26 | print("Room not found for event ${matrixEvent.eventId}"); 27 | return null; 28 | } 29 | // Convert MatrixEvent to Event 30 | final Event finalEvent = await Event.fromMatrixEvent(matrixEvent, room); 31 | // Decrypt the event 32 | final Event decryptedEvent = await encryption.decryptRoomEvent(roomId, finalEvent); 33 | // Check if the decrypted event is now a message 34 | if (decryptedEvent.type == EventTypes.Message && decryptedEvent.content['msgtype'] != null) { 35 | // Skip message if originated from self 36 | if (decryptedEvent.senderId == client.userID) { 37 | return null; 38 | } 39 | final messageData = { 40 | 'eventId': decryptedEvent.eventId, 41 | 'sender': decryptedEvent.senderId, 42 | 'content': decryptedEvent.content, 43 | 'timestamp': decryptedEvent.originServerTs, 44 | }; 45 | 46 | // Attempt to parse location message 47 | await _handleLocationMessageIfAny(messageData); 48 | return messageData; 49 | } 50 | // Not a message, return null 51 | return null; 52 | } 53 | 54 | 55 | /// Handle location message if it's detected 56 | Future _handleLocationMessageIfAny(Map messageData) async { 57 | final sender = messageData['sender'] as String?; 58 | final rawTimestamp = messageData['timestamp']; 59 | final timestamp = rawTimestamp is DateTime 60 | ? rawTimestamp.toIso8601String() 61 | : rawTimestamp?.toString(); 62 | 63 | if (sender == null || timestamp == null) { 64 | print('Invalid message sender or timestamp'); 65 | return; 66 | } 67 | 68 | final locationData = messageParser.parseLocationMessage(messageData); 69 | if (locationData != null) { 70 | final userLocation = UserLocation( 71 | userId: sender, 72 | latitude: locationData['latitude']!, 73 | longitude: locationData['longitude']!, 74 | timestamp: timestamp, 75 | iv: '', // IV is generated or handled in the repository 76 | ); 77 | 78 | await locationRepository.insertLocation(userLocation); 79 | print('Location saved for user: $sender'); 80 | var confirm = await locationRepository.getLatestLocation(sender); 81 | } else { 82 | // It's a message, but not a location message 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/services/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 5 | import 'package:matrix/matrix.dart'; 6 | import 'package:collection/collection.dart'; 7 | import 'package:grid_frontend/repositories/location_repository.dart'; 8 | import 'package:grid_frontend/models/grid_user.dart'; 9 | import 'package:grid_frontend/utilities/utils.dart'; 10 | import 'package:grid_frontend/repositories/sharing_preferences_repository.dart'; 11 | import 'package:http/http.dart' as http; 12 | 13 | enum RelationshipStatus { 14 | alreadyFriends, 15 | invitationSent, 16 | canInvite 17 | } 18 | 19 | class UserService { 20 | final Client client; 21 | final LocationRepository locationRepository; 22 | final SharingPreferencesRepository sharingPreferencesRepository; 23 | 24 | UserService(this.client, this.locationRepository, this.sharingPreferencesRepository); 25 | 26 | Future userExists(String userId) async { 27 | try { 28 | print("Checking if $userId exists."); 29 | final response = await client.getUserProfile(userId); 30 | return response != null; 31 | } catch (e) { 32 | print('Error checking user existence: $e'); 33 | return false; 34 | } 35 | } 36 | 37 | Future getMyUserId() async { 38 | return client.userID; 39 | } 40 | 41 | Future getRelationshipStatus(String effectiveUserId, String targetUserId) async { 42 | 43 | for (var room in client.rooms) { 44 | final roomName1 = "Grid:Direct:$effectiveUserId:$targetUserId"; 45 | final roomName2 = "Grid:Direct:$targetUserId:$effectiveUserId"; 46 | 47 | if (room.name == roomName1 || room.name == roomName2) { 48 | final participants = await room.getParticipants(); 49 | 50 | final User? userMember = participants.firstWhereOrNull( 51 | (user) => user.id == targetUserId, 52 | ); 53 | 54 | final User? ownMember = participants.firstWhereOrNull( 55 | (user) => user.id == client.userID, 56 | ); 57 | 58 | if (userMember != null && ownMember != null) { 59 | if (userMember.membership == Membership.join && 60 | ownMember.membership == Membership.join) { 61 | return RelationshipStatus.alreadyFriends; 62 | } else if (userMember.membership == Membership.invite || 63 | ownMember.membership == Membership.invite) { 64 | return RelationshipStatus.invitationSent; 65 | } 66 | } 67 | } 68 | } 69 | 70 | return RelationshipStatus.canInvite; 71 | } 72 | 73 | Future getLastSeenTime(String userId) async { 74 | try { 75 | final location = await locationRepository.getLatestLocationFromHistory(userId); 76 | if (location == null) return 'Offline'; 77 | final lastTimestamp = location.timestamp; 78 | if (lastTimestamp == "null") return 'Offline'; 79 | final lastSeen = DateTime.parse(lastTimestamp); 80 | return timeAgo(lastSeen); // Use the utility function 81 | } catch (e) { 82 | print("Error fetching last seen time for user $userId: $e"); 83 | return 'Offline'; 84 | } 85 | } 86 | 87 | Future isUserInvited(String roomId, String userId) async { 88 | Room? room = client.getRoomById(roomId); 89 | if (room != null) { 90 | var participants = room.getParticipants(); 91 | return participants.any( 92 | (user) => user.id == userId && user.membership == Membership.invite); 93 | } 94 | return false; 95 | } 96 | 97 | Future checkUsernameAvailability(String username) async { 98 | try { 99 | var response = await http.post( 100 | Uri.parse('${dotenv.env['GAUTH_URL']}/username'), 101 | headers: {'Content-Type': 'application/json'}, 102 | body: jsonEncode({ 103 | 'username': username, 104 | 'phone_number': '+10000000000', 105 | }), 106 | ); 107 | 108 | return response.statusCode == 200; 109 | } catch (e) { 110 | print('Error checking username availability: $e'); 111 | return false; 112 | } 113 | } 114 | 115 | Future isGroupInSharingWindow(String roomId) async { 116 | final sharingPreferences = await sharingPreferencesRepository.getSharingPreferences(roomId, 'group'); 117 | 118 | if (sharingPreferences == null) { 119 | // If no preferences are set, default to sharing (true) 120 | return true; 121 | } 122 | 123 | // If "Always Share" is active, no need to check windows 124 | if (sharingPreferences.activeSharing) { 125 | return true; 126 | } 127 | 128 | // Get the current time and day of the week 129 | final now = DateTime.now(); 130 | final currentDay = now.weekday - 1; // Convert to 0=Monday, 6=Sunday 131 | final currentTime = TimeOfDay.fromDateTime(now); 132 | 133 | // Check if current time falls within any active sharing windows 134 | for (final window in sharingPreferences.shareWindows ?? []) { 135 | if (window.isActive && window.days.contains(currentDay)) { 136 | if (window.isAllDay || 137 | (window.startTime != null && 138 | window.endTime != null && 139 | isTimeInRange(currentTime, window.startTime!, window.endTime!))) { 140 | return true; 141 | } 142 | } 143 | } 144 | 145 | // If no valid sharing window is found 146 | return false; 147 | } 148 | 149 | Future isInSharingWindow(String userId) async { 150 | // Try 'user' type first (new format), then fall back to 'contact' for backwards compatibility 151 | var sharingPreferences = await sharingPreferencesRepository.getSharingPreferences(userId, 'user'); 152 | sharingPreferences ??= await sharingPreferencesRepository.getSharingPreferences(userId, 'contact'); 153 | 154 | if (sharingPreferences == null) { 155 | // If no preferences are set, default to sharing (true) 156 | return true; 157 | } 158 | 159 | // If "Always Share" is active, no need to check windows 160 | if (sharingPreferences.activeSharing) { 161 | return true; 162 | } 163 | 164 | // Get the current time and day of the week 165 | final now = DateTime.now(); 166 | final currentDay = now.weekday - 1; // Convert to 0=Monday, 6=Sunday 167 | final currentTime = TimeOfDay.fromDateTime(now); 168 | 169 | // Check if current time falls within any active sharing windows 170 | for (final window in sharingPreferences.shareWindows ?? []) { 171 | if (window.isActive && window.days.contains(currentDay)) { 172 | if (window.isAllDay || 173 | (window.startTime != null && 174 | window.endTime != null && 175 | isTimeInRange(currentTime, window.startTime!, window.endTime!))) { 176 | return true; 177 | } 178 | } 179 | } 180 | 181 | // If no valid sharing window is found 182 | return false; 183 | } 184 | 185 | /// Helper to check if a time falls within a given range 186 | bool isTimeInRange(TimeOfDay current, String startTime, String endTime) { 187 | final start = _timeOfDayFromString(startTime); 188 | final end = _timeOfDayFromString(endTime); 189 | 190 | if (start.hour < end.hour || 191 | (start.hour == end.hour && start.minute < end.minute)) { 192 | // Normal range (e.g., 09:00 to 17:00) 193 | return (current.hour > start.hour || 194 | (current.hour == start.hour && current.minute >= start.minute)) && 195 | (current.hour < end.hour || 196 | (current.hour == end.hour && current.minute <= end.minute)); 197 | } else { 198 | // Overnight range (e.g., 22:00 to 06:00) 199 | return (current.hour > start.hour || 200 | (current.hour == start.hour && current.minute >= start.minute)) || 201 | (current.hour < end.hour || 202 | (current.hour == end.hour && current.minute <= end.minute)); 203 | } 204 | } 205 | 206 | /// Helper to convert time string (e.g., "09:00") to TimeOfDay 207 | TimeOfDay _timeOfDayFromString(String timeString) { 208 | final parts = timeString.split(':'); 209 | return TimeOfDay( 210 | hour: int.parse(parts[0]), 211 | minute: int.parse(parts[1]), 212 | ); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /lib/styles/themes.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rezivure/Grid-Mobile/c6d6feb3f75f29bb8d0854ce04cb547974e689ec/lib/styles/themes.dart -------------------------------------------------------------------------------- /lib/utilities/encryption_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:encrypt/encrypt.dart' as encrypt; 2 | 3 | String encryptText(String text, String encryptionKey, encrypt.IV iv) { 4 | final key = encrypt.Key.fromBase64(encryptionKey); 5 | final encrypter = encrypt.Encrypter(encrypt.AES(key)); 6 | return encrypter.encrypt(text, iv: iv).base64; 7 | } 8 | 9 | String decryptText(String encryptedText, String encryptionKey, String ivString) { 10 | final key = encrypt.Key.fromBase64(encryptionKey); 11 | final iv = encrypt.IV.fromBase64(ivString); 12 | final encrypter = encrypt.Encrypter(encrypt.AES(key)); 13 | return encrypter.decrypt64(encryptedText, iv: iv); 14 | } 15 | -------------------------------------------------------------------------------- /lib/utilities/message_parser.dart: -------------------------------------------------------------------------------- 1 | 2 | class MessageParser { 3 | Map? parseLocationMessage(Map messageData) { 4 | try { 5 | final content = messageData['content'] as Map?; 6 | if (content == null || content['msgtype'] != 'm.location') { 7 | print('Invalid or non-location message'); 8 | return null; 9 | } 10 | 11 | final geoUri = content['geo_uri'] as String?; 12 | if (geoUri == null || !geoUri.startsWith('geo:')) { 13 | print('Invalid geo_uri format'); 14 | return null; 15 | } 16 | 17 | final coordinates = _parseGeoUri(geoUri); 18 | if (coordinates != null) { 19 | return coordinates; 20 | } 21 | } catch (e) { 22 | print('Error parsing location message: $e'); 23 | return null; 24 | } 25 | } 26 | 27 | Map? _parseGeoUri(String geoUri) { 28 | final parts = geoUri.substring(4).split(','); 29 | if (parts.length < 2) return null; 30 | 31 | final latitude = double.tryParse(parts[0]); 32 | final longitude = double.tryParse(parts[1]); 33 | 34 | if (latitude != null && longitude != null) { 35 | return {'latitude': latitude, 'longitude': longitude}; 36 | } 37 | 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/utilities/time_ago_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TimeAgoFormatter { 4 | static String format(String? timestamp) { 5 | if (timestamp == null || timestamp == 'Offline') { 6 | return 'Off Grid'; 7 | } 8 | 9 | try { 10 | final lastSeenDateTime = DateTime.parse(timestamp).toLocal(); 11 | final now = DateTime.now(); 12 | 13 | if (lastSeenDateTime.isAfter(now)) { 14 | print("Warning: Future timestamp detected: $timestamp"); 15 | return 'Off Grid'; 16 | } 17 | 18 | final difference = now.difference(lastSeenDateTime); 19 | 20 | if (difference.inSeconds < 30) { 21 | return 'Just now'; 22 | } else if (difference.inMinutes < 1) { 23 | return '${difference.inSeconds}s ago'; 24 | } else if (difference.inHours < 1) { 25 | return '${difference.inMinutes}m ago'; 26 | } else if (difference.inHours < 24) { 27 | return '${difference.inHours}h ago'; 28 | } else if (difference.inDays < 7) { 29 | return '${difference.inDays}d ago'; 30 | } else { 31 | return 'Off Grid'; 32 | } 33 | } catch (e) { 34 | print("Error parsing timestamp: $e"); 35 | return 'Off Grid'; 36 | } 37 | } 38 | 39 | static Color getStatusColor(String timeAgoText, ColorScheme colorScheme) { 40 | if (timeAgoText == 'Off Grid' || timeAgoText == 'Invitation Sent') { 41 | return colorScheme.onSurface.withOpacity(0.5); 42 | } else if (timeAgoText.contains('m ago') || 43 | timeAgoText.contains('s ago') || 44 | timeAgoText == 'Just now') { 45 | return colorScheme.primary; 46 | } else if (timeAgoText.contains('h ago')) { 47 | return Colors.yellow; 48 | } else { 49 | return Colors.red; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /lib/utilities/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 4 | 5 | 6 | Color generateColorFromUsername(String username) { 7 | final random = Random(username.hashCode); 8 | 9 | Color primaryColor = Color(0xFF00DBA4); // Caribbean Green 10 | Color secondaryColor = Color(0xFF267373); // Oracle 11 | 12 | // Mix the primary and secondary colors based on the username hash 13 | double mixFactor = random.nextDouble() * 20; 14 | Color mixedColor = Color.lerp(primaryColor, secondaryColor, mixFactor)!; 15 | 16 | // Optionally adjust brightness and saturation further 17 | HSLColor hslColor = HSLColor.fromColor(mixedColor); 18 | hslColor = hslColor.withLightness(0.6); // Adjust lightness to enhance the professional feel 19 | hslColor = hslColor.withSaturation(0.5); // Reduce saturation 20 | 21 | return hslColor.toColor(); 22 | } 23 | 24 | String getFirstLetter(String username) { 25 | return username.isNotEmpty ? username.replaceAll('@', '')[0].toUpperCase() : ''; 26 | } 27 | 28 | String parseGroupName(String roomName) { 29 | const prefix = "Grid Group "; 30 | const suffix = " with "; 31 | 32 | if (roomName.startsWith(prefix) && roomName.contains(suffix)) { 33 | final startIndex = prefix.length; 34 | final endIndex = roomName.indexOf(suffix, startIndex); 35 | return roomName.substring(startIndex, endIndex); 36 | } 37 | 38 | // Default case: return the first 12 characters if no prefix/suffix found 39 | return roomName.length > 12 ? roomName.substring(0, 12) : roomName; 40 | } 41 | 42 | String localpart(String userId) { 43 | return userId.split(":").first.replaceFirst('@', ''); 44 | } 45 | 46 | /// Converts a `DateTime` into a human-readable "time ago" string. 47 | String timeAgo(DateTime lastSeen) { 48 | final now = DateTime.now(); 49 | final difference = now.difference(lastSeen); 50 | 51 | if (difference.inSeconds < 60) { 52 | return '${difference.inSeconds}s ago'; 53 | } else if (difference.inMinutes < 60) { 54 | return '${difference.inMinutes}m ago'; 55 | } else if (difference.inHours < 24) { 56 | return '${difference.inHours}h ago'; 57 | } else { 58 | return '${difference.inDays}d ago'; 59 | } 60 | } 61 | 62 | 63 | 64 | /// Utility function to check if a room is a direct room based on its name. 65 | /// Assumes direct room names follow the format: "Grid:Direct::" 66 | bool isDirectRoom(String roomName) { 67 | // Check if the room name starts with "Grid:Direct:" 68 | if (!roomName.startsWith("Grid:Direct:")) { 69 | return false; 70 | } 71 | 72 | // Extract the remaining part after "Grid:Direct:" 73 | final remainingPart = roomName.substring("Grid:Direct:".length); 74 | 75 | // Split the remaining part into users by ":" 76 | final userParts = remainingPart.split(':'); 77 | 78 | // Check if there are exactly two user identifiers 79 | return userParts.length == 4; 80 | } 81 | 82 | /// Utility to extract expiration timestamp from a room name. 83 | /// Room name format: "Grid:Group:::" 84 | /// Returns 0 if the room never expires or the format is invalid. 85 | int extractExpirationTimestamp(String roomName) { 86 | final parts = roomName.split(':'); 87 | 88 | if (parts.length < 3) { 89 | // If the room name doesn't have enough parts, assume no expiration. 90 | return 0; 91 | } 92 | 93 | if (parts[0] == 'Grid' && parts[1] == 'Group') { 94 | final expirationPart = parts[2]; 95 | return int.tryParse(expirationPart) ?? 0; // Default to 0 if parsing fails. 96 | } 97 | 98 | // Return 0 if the room is not a group or the format is invalid. 99 | return 0; 100 | } 101 | 102 | String formatUserId(String userId) { 103 | // Default homeserver fallback in case dotenv fails 104 | const FALLBACK_DEFAULT_HOMESERVER = 'matrix.mygrid.app'; 105 | 106 | final homeserver = dotenv.env['HOMESERVER'] ?? FALLBACK_DEFAULT_HOMESERVER; 107 | 108 | // Split the userId into localpart and domain 109 | final parts = userId.split(':'); 110 | if (parts.length != 2) return userId; 111 | 112 | final domain = parts[1]; 113 | 114 | // If domain matches homeserver from .env or fallback, return only localpart 115 | // Otherwise return full userId 116 | return (domain == homeserver || domain == FALLBACK_DEFAULT_HOMESERVER) ? parts[0] : userId; 117 | } 118 | 119 | bool isCustomHomeserver(String currentHomeserver) { 120 | // Default homeserver fallback in case dotenv fails 121 | const FALLBACK_DEFAULT_HOMESERVER = 'matrix.mygrid.app'; 122 | 123 | final defaultHomeserver = dotenv.env['HOMESERVER'] ?? FALLBACK_DEFAULT_HOMESERVER; 124 | 125 | // Handle empty or null-like strings 126 | if (currentHomeserver.isEmpty || currentHomeserver == 'null') { 127 | print('Warning: Empty or null homeserver provided, assuming default'); 128 | return false; 129 | } 130 | 131 | // Clean up the current homeserver URL 132 | final cleanedHomeserver = currentHomeserver 133 | .replaceFirst('https://', '') 134 | .replaceFirst('http://', '') 135 | .replaceFirst(':443', '') // Remove default HTTPS port 136 | .replaceFirst(':80', ''); // Remove default HTTP port 137 | 138 | // If dotenv didn't load properly, also check against the fallback 139 | if (dotenv.env['HOMESERVER'] == null) { 140 | print('Warning: HOMESERVER env var not found, using fallback'); 141 | } 142 | 143 | print('isCustomHomeserver check: cleaned=$cleanedHomeserver, default=$defaultHomeserver'); 144 | 145 | return cleanedHomeserver != defaultHomeserver && cleanedHomeserver != FALLBACK_DEFAULT_HOMESERVER; 146 | } 147 | 148 | -------------------------------------------------------------------------------- /lib/widgets/app_initializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:matrix/matrix.dart'; 4 | import '../screens/onboarding/welcome_screen.dart'; 5 | import '../screens/map/map_tab.dart'; 6 | 7 | class AppInitializer extends StatefulWidget { 8 | final Client client; 9 | 10 | const AppInitializer({Key? key, required this.client}) : super(key: key); 11 | 12 | @override 13 | _AppInitializerState createState() => _AppInitializerState(); 14 | } 15 | 16 | class _AppInitializerState extends State 17 | with SingleTickerProviderStateMixin { 18 | late AnimationController _fadeController; 19 | late Animation _fadeAnimation; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | 25 | // Simple fade and scale animation 26 | _fadeController = AnimationController( 27 | duration: const Duration(milliseconds: 600), 28 | vsync: this, 29 | ); 30 | 31 | _fadeAnimation = CurvedAnimation( 32 | parent: _fadeController, 33 | curve: Curves.easeOut, 34 | ); 35 | 36 | // Start simple fade in 37 | _fadeController.forward(); 38 | 39 | _initializeApp(); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _fadeController.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | 49 | Future _initializeApp() async { 50 | // Show splash for minimum time to ensure smooth UX 51 | await Future.delayed(const Duration(milliseconds: 1500)); 52 | 53 | if (!mounted) return; 54 | 55 | // Check authentication state 56 | if (widget.client.isLogged()) { 57 | // User is logged in, go to main app 58 | Navigator.pushReplacement( 59 | context, 60 | PageRouteBuilder( 61 | pageBuilder: (context, animation, secondaryAnimation) => const MapTab(), 62 | transitionDuration: const Duration(milliseconds: 300), 63 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 64 | return FadeTransition(opacity: animation, child: child); 65 | }, 66 | ), 67 | ); 68 | } else { 69 | // User not logged in, go directly to welcome screen 70 | Navigator.pushReplacement( 71 | context, 72 | PageRouteBuilder( 73 | pageBuilder: (context, animation, secondaryAnimation) => WelcomeScreen(), 74 | transitionDuration: const Duration(milliseconds: 300), 75 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 76 | return FadeTransition(opacity: animation, child: child); 77 | }, 78 | ), 79 | ); 80 | } 81 | } 82 | 83 | Widget _buildModernLogo() { 84 | return Container( 85 | width: 120, 86 | height: 120, 87 | child: Image.asset( 88 | 'assets/logos/png-file-2.png', 89 | fit: BoxFit.contain, 90 | ), 91 | ); 92 | } 93 | 94 | 95 | 96 | @override 97 | Widget build(BuildContext context) { 98 | final theme = Theme.of(context); 99 | final colorScheme = theme.colorScheme; 100 | 101 | return Scaffold( 102 | backgroundColor: colorScheme.surface, 103 | body: FadeTransition( 104 | opacity: _fadeAnimation, 105 | child: Center( 106 | child: _buildModernLogo(), 107 | ), 108 | ), 109 | ); 110 | } 111 | } -------------------------------------------------------------------------------- /lib/widgets/custom_search_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomSearchBar extends StatelessWidget { 4 | final TextEditingController controller; 5 | final String hintText; 6 | 7 | CustomSearchBar({required this.controller, this.hintText = 'Search'}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.all(8.0), 13 | child: Container( 14 | height: 40.0, // Adjust the height as needed 15 | child: TextField( 16 | controller: controller, 17 | decoration: InputDecoration( 18 | hintText: hintText, 19 | prefixIcon: Icon(Icons.search), 20 | border: OutlineInputBorder( 21 | borderRadius: BorderRadius.circular(8.0), 22 | borderSide: BorderSide.none, 23 | ), 24 | filled: true, 25 | contentPadding: EdgeInsets.all(8.0), 26 | ), 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/widgets/group_info_subscreen.dart: -------------------------------------------------------------------------------- 1 | // group_info_subscreen.dart 2 | import 'package:flutter/material.dart'; 3 | 4 | class GroupInfoSubscreen extends StatelessWidget { 5 | final String groupName; 6 | final VoidCallback onBack; 7 | final ScrollController scrollController; 8 | 9 | GroupInfoSubscreen({ 10 | required this.groupName, 11 | required this.onBack, 12 | required this.scrollController, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final theme = Theme.of(context); 18 | final colorScheme = theme.colorScheme; 19 | 20 | return Column( 21 | children: [ 22 | ListTile( 23 | leading: Icon(Icons.arrow_back), 24 | title: Text('Back'), 25 | onTap: onBack, 26 | ), 27 | Text( 28 | groupName, 29 | style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), 30 | ), 31 | Expanded( 32 | child: ListView.builder( 33 | controller: scrollController, 34 | itemCount: 5, // Mock number of group members, replace with actual data 35 | itemBuilder: (context, index) { 36 | return ListTile( 37 | title: Text('Group Member $index'), 38 | ); 39 | }, 40 | ), 41 | ), 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/widgets/groups_subscreen.dart: -------------------------------------------------------------------------------- 1 | // lib/widgets/groups_subscreen.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:grid_frontend/widgets/group_info_subscreen.dart'; 5 | import 'package:grid_frontend/widgets/custom_search_bar.dart'; 6 | 7 | class GroupsSubscreen extends StatefulWidget { 8 | final ScrollController scrollController; 9 | 10 | GroupsSubscreen({required this.scrollController}); 11 | 12 | @override 13 | _GroupsSubscreenState createState() => _GroupsSubscreenState(); 14 | } 15 | 16 | class _GroupsSubscreenState extends State { 17 | bool _showGroupDetail = false; 18 | String _selectedGroupName = ''; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final theme = Theme.of(context); 23 | final colorScheme = theme.colorScheme; 24 | 25 | return Column( 26 | children: [ 27 | CustomSearchBar( 28 | controller: TextEditingController(), hintText: 'Search Groups'), 29 | if (_showGroupDetail) 30 | Expanded( 31 | child: GroupInfoSubscreen( 32 | groupName: _selectedGroupName, 33 | onBack: () { 34 | setState(() { 35 | _showGroupDetail = false; 36 | _selectedGroupName = ''; 37 | }); 38 | }, 39 | scrollController: widget.scrollController, 40 | ), 41 | ) 42 | else 43 | Expanded( 44 | child: ListView.builder( 45 | controller: widget.scrollController, 46 | itemCount: 10, // Replace with the actual number of groups 47 | padding: EdgeInsets.only(top: 8.0), 48 | itemBuilder: (context, index) { 49 | return Column( 50 | children: [ 51 | ListTile( 52 | leading: CircleAvatar( 53 | radius: 30, 54 | child: Text('G'), 55 | backgroundColor: colorScheme.primary.withOpacity(0.2), 56 | ), 57 | title: Text( 58 | 'Group Name $index', 59 | style: TextStyle(color: colorScheme.onBackground), 60 | ), 61 | subtitle: Text( 62 | 'Last activity info', 63 | style: TextStyle(color: colorScheme.onSurface), 64 | ), 65 | onTap: () { 66 | setState(() { 67 | _showGroupDetail = true; 68 | _selectedGroupName = 'Group Name $index'; 69 | }); 70 | }, 71 | ), 72 | Divider( 73 | thickness: 1, 74 | color: colorScheme.onSurface.withOpacity(0.1), 75 | indent: 20, 76 | endIndent: 20, 77 | ), 78 | ], 79 | ); 80 | }, 81 | ), 82 | ), 83 | ], 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/widgets/status_indictator.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../utilities/time_ago_formatter.dart'; 6 | 7 | class StatusIndicator extends StatelessWidget { 8 | final String timeAgo; 9 | final String? membershipStatus; 10 | 11 | const StatusIndicator({ 12 | Key? key, 13 | required this.timeAgo, 14 | this.membershipStatus, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final theme = Theme.of(context); 20 | final colorScheme = theme.colorScheme; 21 | 22 | // Handle membership status first 23 | if (membershipStatus == 'invite') { 24 | return Container( 25 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 26 | decoration: BoxDecoration( 27 | color: Colors.orange.withOpacity(0.1), 28 | borderRadius: BorderRadius.circular(8), 29 | border: Border.all( 30 | color: Colors.orange.withOpacity(0.2), 31 | width: 1, 32 | ), 33 | ), 34 | child: Row( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Icon( 38 | Icons.mail_outline, 39 | size: 12, 40 | color: Colors.orange, 41 | ), 42 | const SizedBox(width: 4), 43 | Text( 44 | 'Invitation Sent', 45 | style: theme.textTheme.bodySmall?.copyWith( 46 | color: Colors.orange, 47 | fontWeight: FontWeight.w500, 48 | fontSize: 11, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | 56 | // Get status color and text 57 | Color statusColor = _getStatusColor(timeAgo, colorScheme); 58 | IconData statusIcon = _getStatusIcon(timeAgo); 59 | String enhancedText = _getEnhancedStatusText(timeAgo); 60 | 61 | // Handle regular time ago status with enhanced styling 62 | return Container( 63 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 64 | decoration: BoxDecoration( 65 | color: statusColor.withOpacity(0.1), 66 | borderRadius: BorderRadius.circular(8), 67 | border: Border.all( 68 | color: statusColor.withOpacity(0.2), 69 | width: 1, 70 | ), 71 | ), 72 | child: Row( 73 | mainAxisSize: MainAxisSize.min, 74 | children: [ 75 | Icon( 76 | statusIcon, 77 | size: 12, 78 | color: statusColor, 79 | ), 80 | const SizedBox(width: 4), 81 | Text( 82 | enhancedText, 83 | style: theme.textTheme.bodySmall?.copyWith( 84 | color: statusColor, 85 | fontWeight: FontWeight.w500, 86 | fontSize: 11, 87 | ), 88 | ), 89 | ], 90 | ), 91 | ); 92 | } 93 | 94 | Color _getStatusColor(String timeAgo, ColorScheme colorScheme) { 95 | if (timeAgo == 'Just now' || timeAgo.contains('s ago')) { 96 | return colorScheme.primary; // Use primary green 97 | } else if (timeAgo.contains('m ago') && !timeAgo.contains('h')) { 98 | // Extract minutes to check if over 10 minutes 99 | final minutesMatch = RegExp(r'(\d+)m ago').firstMatch(timeAgo); 100 | if (minutesMatch != null) { 101 | final minutes = int.parse(minutesMatch.group(1)!); 102 | return minutes <= 10 ? colorScheme.primary : Colors.orange; 103 | } 104 | return colorScheme.primary; 105 | } else if (timeAgo.contains('h ago')) { 106 | return Colors.orange; 107 | } else if (timeAgo.contains('d ago')) { 108 | return Colors.red; 109 | } else { 110 | return colorScheme.onSurface.withOpacity(0.4); 111 | } 112 | } 113 | 114 | IconData _getStatusIcon(String timeAgo) { 115 | if (timeAgo == 'Just now' || timeAgo.contains('s ago')) { 116 | return Icons.circle; 117 | } else if (timeAgo.contains('m ago') && !timeAgo.contains('h')) { 118 | return Icons.circle; 119 | } else if (timeAgo.contains('h ago')) { 120 | return Icons.schedule; 121 | } else if (timeAgo.contains('d ago')) { 122 | return Icons.access_time; 123 | } else { 124 | return Icons.circle_outlined; 125 | } 126 | } 127 | 128 | String _getEnhancedStatusText(String timeAgo) { 129 | if (timeAgo == 'Just now') { 130 | return 'Active now'; 131 | } else if (timeAgo.contains('s ago')) { 132 | return 'Active now'; 133 | } else if (timeAgo.contains('m ago') && !timeAgo.contains('h')) { 134 | return timeAgo; 135 | } else if (timeAgo.contains('h ago')) { 136 | return timeAgo; 137 | } else if (timeAgo.contains('d ago')) { 138 | return timeAgo; 139 | } else { 140 | return 'Offline'; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/widgets/triangle_avatars.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:random_avatar/random_avatar.dart'; 3 | import '../utilities/utils.dart'; 4 | import 'two_user_avatars.dart'; 5 | 6 | class TriangleAvatars extends StatelessWidget { 7 | final List userIds; 8 | 9 | const TriangleAvatars({super.key, required this.userIds}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final colorScheme = Theme.of(context).colorScheme; 14 | 15 | // Handle empty case 16 | if (userIds.isEmpty) { 17 | return CircleAvatar( 18 | radius: 30, 19 | backgroundColor: colorScheme.primary.withOpacity(0.2), 20 | child: Icon( 21 | Icons.group_off, 22 | color: colorScheme.primary, 23 | size: 30, 24 | ), 25 | ); 26 | } 27 | 28 | // Handle single member case 29 | if (userIds.length == 1) { 30 | return CircleAvatar( 31 | radius: 30, 32 | backgroundColor: colorScheme.primary.withOpacity(0.1), 33 | child: Stack( 34 | alignment: Alignment.center, 35 | children: [ 36 | RandomAvatar( 37 | localpart(userIds[0]), 38 | height: 40, 39 | width: 40, 40 | ), 41 | 42 | ], 43 | ), 44 | ); 45 | } 46 | 47 | // Handle two member case 48 | if (userIds.length == 2) { 49 | return TwoUserAvatars(userIds: userIds); 50 | } 51 | 52 | // Handle three or more members case 53 | List displayedUserIds = userIds.take(3).toList(); 54 | 55 | return CircleAvatar( 56 | radius: 30, 57 | backgroundColor: Colors.grey.shade200, 58 | child: Stack( 59 | alignment: Alignment.center, 60 | children: [ 61 | Positioned( 62 | top: 6, 63 | child: RandomAvatar( 64 | localpart(displayedUserIds[0]), 65 | height: 28, 66 | width: 28, 67 | ), 68 | ), 69 | Positioned( 70 | bottom: 6, 71 | left: 6, 72 | child: RandomAvatar( 73 | localpart(displayedUserIds[1]), 74 | height: 28, 75 | width: 28, 76 | ), 77 | ), 78 | Positioned( 79 | bottom: 6, 80 | right: 6, 81 | child: RandomAvatar( 82 | localpart(displayedUserIds[2]), 83 | height: 28, 84 | width: 28, 85 | ), 86 | ), 87 | ], 88 | ), 89 | ); 90 | } 91 | } -------------------------------------------------------------------------------- /lib/widgets/two_user_avatars.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:random_avatar/random_avatar.dart'; 3 | 4 | import '../utilities/utils.dart'; 5 | 6 | class TwoUserAvatars extends StatelessWidget { 7 | final List userIds; 8 | 9 | const TwoUserAvatars({super.key, required this.userIds}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final colorScheme = Theme.of(context).colorScheme; 14 | 15 | // Ensure there are at least two distinct avatars 16 | List displayedUserIds = userIds.toSet().toList(); 17 | if (displayedUserIds.length < 2) { 18 | displayedUserIds.add(displayedUserIds[0]); 19 | } 20 | displayedUserIds = displayedUserIds.take(2).toList(); 21 | 22 | return CircleAvatar( 23 | radius: 30, 24 | backgroundColor: colorScheme.primary.withOpacity(0.1), 25 | child: Stack( 26 | alignment: Alignment.center, 27 | children: [ 28 | Positioned( 29 | top: 6, 30 | left: 6, 31 | child: RandomAvatar( 32 | localpart(displayedUserIds[0]), 33 | height: 32, 34 | width: 32, 35 | ), 36 | ), 37 | Positioned( 38 | bottom: 6, 39 | right: 6, 40 | child: RandomAvatar( 41 | localpart(displayedUserIds[1]), 42 | height: 32, 43 | width: 32, 44 | ), 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | } -------------------------------------------------------------------------------- /lib/widgets/user_keys_modal.dart: -------------------------------------------------------------------------------- 1 | // lib/widgets/user_keys_modal.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:grid_frontend/services/database_service.dart'; 6 | import 'package:grid_frontend/services/user_service.dart'; 7 | import 'package:grid_frontend/repositories/user_keys_repository.dart'; 8 | 9 | class UserKeysModal extends StatelessWidget { 10 | final String userId; 11 | final bool approvedKeys; 12 | final UserService userService; 13 | final UserKeysRepository userKeysRepository; 14 | 15 | UserKeysModal({ 16 | required this.userId, 17 | required this.approvedKeys, 18 | required this.userService, 19 | required this.userKeysRepository 20 | }); 21 | 22 | Future?> _fetchDeviceKeys(BuildContext context) async { 23 | return await userKeysRepository.getKeysByUserId(userId); 24 | } 25 | 26 | void approveKeys(BuildContext context) async { 27 | try { 28 | print("attemping to approve keys for ${this.userId}"); 29 | await userKeysRepository.updateApprovedKeys(this.userId, true); 30 | 31 | // Notify the parent widget that keys were approved 32 | Navigator.of(context).pop(true); 33 | 34 | ScaffoldMessenger.of(context).showSnackBar( 35 | SnackBar(content: Text('User keys verified.')), 36 | ); 37 | } catch (e) { 38 | ScaffoldMessenger.of(context).showSnackBar( 39 | SnackBar(content: Text('Failed to verify user keys.')), 40 | ); 41 | } 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final theme = Theme.of(context); 47 | final onSurfaceColor = theme.colorScheme.onSurface; 48 | final surfaceColor = theme.colorScheme.surface; 49 | 50 | return AlertDialog( 51 | title: Text('Safety Number'), 52 | content: Column( 53 | mainAxisSize: MainAxisSize.min, 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | children: [ 56 | SizedBox(height: 10), 57 | Row( 58 | children: [ 59 | Icon( 60 | approvedKeys ? Icons.lock : Icons.lock_open, 61 | color: approvedKeys ? Colors.green : Colors.red, 62 | ), 63 | SizedBox(width: 8), 64 | Text( 65 | approvedKeys ? 'Keys Verified' : 'Safety Number has Changed', 66 | style: TextStyle( 67 | fontSize: 16, 68 | fontWeight: FontWeight.bold, 69 | color: approvedKeys ? Colors.green : Colors.red, 70 | ), 71 | ), 72 | ], 73 | ), 74 | SizedBox(height: 10), 75 | Text( 76 | approvedKeys 77 | ? 'This contact\'s keys are verified and trusted for secure communication.' 78 | : 'This user’s keys have changed. Please verify their device IDs below. ', 79 | style: TextStyle(fontSize: 14), 80 | ), 81 | SizedBox(height: 10), 82 | if (!approvedKeys) 83 | FutureBuilder?>( 84 | future: _fetchDeviceKeys(context), 85 | builder: (context, snapshot) { 86 | if (snapshot.connectionState == ConnectionState.waiting) { 87 | return CircularProgressIndicator(); 88 | } else if (snapshot.hasError) { 89 | return Text( 90 | 'Error loading device keys', 91 | style: TextStyle(color: Colors.red), 92 | ); 93 | } else if (!snapshot.hasData || snapshot.data == null) { 94 | return Text( 95 | 'No device keys found for this user.', 96 | style: TextStyle(color: Colors.grey), 97 | ); 98 | } else { 99 | final deviceKeys = snapshot.data!; 100 | return Column( 101 | crossAxisAlignment: CrossAxisAlignment.start, 102 | children: deviceKeys.keys.map((deviceId) { 103 | return Padding( 104 | padding: const EdgeInsets.symmetric(vertical: 4.0), 105 | child: Text( 106 | 'Device ID: $deviceId', 107 | style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), 108 | ), 109 | ); 110 | }).toList(), 111 | ); 112 | } 113 | }, 114 | ), 115 | ], 116 | ), 117 | actions: [ 118 | ElevatedButton( 119 | style: ElevatedButton.styleFrom( 120 | backgroundColor: surfaceColor, 121 | foregroundColor: Colors.red, 122 | ), 123 | onPressed: () => Navigator.of(context).pop(), 124 | child: Text('Close'), 125 | ), 126 | if (!approvedKeys) 127 | ElevatedButton( 128 | style: ElevatedButton.styleFrom( 129 | backgroundColor: onSurfaceColor, 130 | foregroundColor: surfaceColor, 131 | ), 132 | onPressed: () { 133 | approveKeys(context); 134 | }, 135 | child: Text('Verify Keys'), 136 | ), 137 | ], 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/widgets/version_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:matrix/matrix.dart'; 3 | import 'package:grid_frontend/widgets/version_checker.dart'; 4 | import 'package:package_info_plus/package_info_plus.dart'; 5 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 6 | import 'package:http/http.dart' as http; 7 | import 'dart:convert'; 8 | 9 | class VersionWrapper extends StatefulWidget { 10 | final Widget child; 11 | final Client client; 12 | 13 | const VersionWrapper({ 14 | Key? key, 15 | required this.child, 16 | required this.client, 17 | }) : super(key: key); 18 | 19 | @override 20 | State createState() => _VersionWrapperState(); 21 | } 22 | 23 | class _VersionWrapperState extends State { 24 | bool _needsCriticalUpdate = false; 25 | bool _checkComplete = false; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _checkVersion(); 31 | } 32 | 33 | Future _checkVersion() async { 34 | print('Checking version...'); 35 | try { 36 | final packageInfo = await PackageInfo.fromPlatform(); 37 | final currentVersion = packageInfo.version; 38 | print('Current version: $currentVersion'); 39 | 40 | final versionCheckUrl = dotenv.env['VERSION_CHECK_URL'] ?? ''; 41 | final response = await http.get(Uri.parse(versionCheckUrl)); 42 | 43 | if (response.statusCode == 200) { 44 | final versionInfo = json.decode(response.body); 45 | final minimumVersion = versionInfo['minimum_version']; 46 | final latestVersion = versionInfo['latest_version']; 47 | print('Minimum version: $minimumVersion'); 48 | print('Latest version: $latestVersion'); 49 | 50 | bool isCritical = VersionChecker.isVersionLower(currentVersion, minimumVersion); 51 | bool hasOptionalUpdate = VersionChecker.isVersionLower(currentVersion, latestVersion); 52 | 53 | print('Needs critical update: $isCritical'); 54 | print('Has optional update: $hasOptionalUpdate'); 55 | 56 | if (mounted) { 57 | setState(() { 58 | _needsCriticalUpdate = isCritical; 59 | _checkComplete = true; 60 | }); 61 | 62 | if (isCritical) { 63 | VersionChecker.checkVersion(context); 64 | } else if (hasOptionalUpdate) { 65 | // Show optional update dialog if there's a newer version but not critical 66 | VersionChecker.showOptionalUpdateDialog( 67 | context, 68 | dotenv.env['APP_STORE_URL'] ?? '', 69 | dotenv.env['PLAY_STORE_URL'] ?? '' 70 | ); 71 | } 72 | } 73 | } 74 | } catch (e) { 75 | print('Version check error: $e'); 76 | } finally { 77 | if (mounted) { 78 | setState(() { 79 | _checkComplete = true; 80 | }); 81 | } 82 | } 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | if (!_checkComplete) { 88 | // Show the beautiful splash while checking version 89 | return widget.child; 90 | } 91 | 92 | if (_needsCriticalUpdate) { 93 | return MaterialApp( 94 | theme: Theme.of(context), 95 | home: Scaffold( 96 | backgroundColor: Theme.of(context).colorScheme.background, 97 | body: SafeArea( 98 | child: Center( 99 | child: Column( 100 | mainAxisAlignment: MainAxisAlignment.center, 101 | children: [ 102 | Container( 103 | width: 120, 104 | height: 120, 105 | padding: const EdgeInsets.all(24), 106 | decoration: BoxDecoration( 107 | shape: BoxShape.circle, 108 | gradient: RadialGradient( 109 | colors: [ 110 | Theme.of(context).colorScheme.primary.withOpacity(0.1), 111 | Theme.of(context).colorScheme.primary.withOpacity(0.05), 112 | Colors.transparent, 113 | ], 114 | stops: const [0.3, 0.7, 1.0], 115 | ), 116 | ), 117 | child: Image.asset( 118 | 'assets/logos/png-file-2.png', 119 | fit: BoxFit.contain, 120 | ), 121 | ), 122 | const SizedBox(height: 32), 123 | Text( 124 | 'Update Required', 125 | style: Theme.of(context).textTheme.headlineSmall?.copyWith( 126 | fontWeight: FontWeight.w700, 127 | color: Theme.of(context).colorScheme.onBackground, 128 | ), 129 | ), 130 | const SizedBox(height: 8), 131 | Text( 132 | 'Please update Grid to continue', 133 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 134 | color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), 135 | ), 136 | ), 137 | const SizedBox(height: 24), 138 | CircularProgressIndicator( 139 | color: Theme.of(context).colorScheme.primary, 140 | strokeWidth: 2, 141 | ), 142 | ], 143 | ), 144 | ), 145 | ), 146 | ), 147 | ); 148 | } 149 | 150 | return widget.child; 151 | } 152 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: grid_frontend 2 | description: "Grid: Private Location Sharing" 3 | publish_to: 'none' 4 | version: 1.0.4+107 5 | 6 | environment: 7 | sdk: '>=3.3.3 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | cupertino_icons: ^1.0.6 13 | flutter_map: ^7.0.2 14 | timeago: ^3.1.0 15 | latlong2: ^0.9.1 16 | flutter_map_marker_popup: ^7.0.0 17 | flutter_map_location_marker: ^9.1.1 18 | flutter_map_marker_cluster: ^1.3.6 19 | provider: ^6.1.2 20 | shared_preferences: ^2.2.3 21 | matrix: ^0.29.8 22 | flutter_olm: ^2.0.0 23 | flutter_openssl_crypto: ^0.5.0 24 | flutter_secure_storage: ^9.2.2 25 | flutter_emoji: ^2.5.1 26 | path_provider: ^2.1.4 27 | sqflite: ^2.3.3+1 28 | crypto: ^3.0.4 29 | encrypt: ^5.0.3 30 | flutter_map_cancellable_tile_provider: ^3.0.2 # Downgraded to a compatible version 31 | flutter_launcher_icons: ^0.14.2 32 | random_avatar: any # or the latest version on Pub 33 | url_launcher: ^6.3.0 34 | http: ^1.2.2 35 | vector_map_tiles: ^8.0.0 36 | vector_map_tiles_pmtiles: ^1.3.0 37 | flutter_intl_phone_field: ^0.0.7 38 | lottie: ^3.1.2 39 | sleek_circular_slider: ^2.0.1 40 | flutter_slidable: ^3.1.1 41 | flutter_dotenv: ^5.1.0 42 | permission_handler: ^11.3.1 43 | qr_flutter: ^4.1.0 44 | flutter_bloc: ^8.1.6 45 | collection: ^1.18.0 46 | equatable: ^2.0.7 47 | flutter_background_geolocation: ^4.16.5 48 | hive: ^2.2.3 49 | qr_code_scanner_plus: ^2.0.6 50 | package_info_plus: ^8.1.2 51 | 52 | 53 | flutter_launcher_icons: 54 | android: true 55 | adaptive_icon_background: "#FFFFFF" 56 | adaptive_icon_foreground: "assets/appIcons/playstore.png" 57 | adaptive_icon_padding: false # 58 | ios: true 59 | image_path: "assets/appIcons/appstore.png" 60 | min_sdk_android: 21 61 | 62 | dev_dependencies: 63 | flutter_test: 64 | sdk: flutter 65 | flutter_native_splash: ^2.3.10 66 | flutter_lints: ^5.0.0 67 | 68 | flutter: 69 | uses-material-design: true 70 | assets: 71 | - assets/logos/Jpeg-2.jpg 72 | - assets/logos/png-file-2.png 73 | - assets/logos/Brand-Pattern-Full-Color.png 74 | - assets/logos/Brand-Pattern-Low-color.png 75 | - assets/lottie/lottie-phone-pin.json 76 | - assets/lottie/lottie-phone-verify.json 77 | - .env 78 | 79 | fonts: 80 | - family: GridFont 81 | fonts: 82 | - asset: assets/fonts/fontawesome-webfont.ttf 83 | weight: 700 84 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:grid_frontend/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------