initial
5
.firebaserc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "cyberhybridhub"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.flutter-plugins-dependencies
Normal file
80
.gitignore
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Flutter/Dart
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# VS Code (keep .vscode/ committed for team settings)
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Android
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/android/key.properties
|
||||||
|
*.jks
|
||||||
|
|
||||||
|
# iOS/macOS
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/ephemeral
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Web
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
*.env
|
||||||
|
*.env.*
|
||||||
45
.metadata
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: android
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: web
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
6
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Dart-Code.dart-code",
|
||||||
|
"Dart-Code.flutter"
|
||||||
|
]
|
||||||
|
}
|
||||||
28
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Flutter (web)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"deviceId": "web-server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flutter (debug)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flutter (profile)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flutter (release)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"dart.flutterSdkPath": "/home/nathan/github-open/flutter",
|
||||||
|
"terminal.integrated.env.linux": {
|
||||||
|
"PATH": "/home/nathan/github-open/flutter/bin:${env:PATH}"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.rulers": [80],
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.dart_tool": true,
|
||||||
|
"**/.packages": true,
|
||||||
|
"build": true
|
||||||
|
},
|
||||||
|
"[dart]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.selectionHighlight": false,
|
||||||
|
"editor.suggestSelection": "first",
|
||||||
|
"editor.tabCompletion": "onlySnippets",
|
||||||
|
"editor.wordBasedSuggestions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# cyberhybridhub
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||||
|
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
238
TODO.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# Firebase Google Sign-In setup
|
||||||
|
|
||||||
|
## Quick setup (Ubuntu CLI)
|
||||||
|
|
||||||
|
Reload your shell after the first run (or open a new terminal):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
One-shot setup (installs tools, logs in, configures FlutterFire, registers debug SHA-1):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/cyberhybridhub.com
|
||||||
|
chmod +x scripts/setup-firebase-google-auth.sh scripts/get-android-sha.sh
|
||||||
|
|
||||||
|
# Optional: set your Firebase project id to skip the interactive picker
|
||||||
|
export FIREBASE_PROJECT_ID=your-firebase-project-id
|
||||||
|
|
||||||
|
./scripts/setup-firebase-google-auth.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run step by step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Tools (if not already installed)
|
||||||
|
npm install -g firebase-tools
|
||||||
|
dart pub global activate flutterfire_cli
|
||||||
|
|
||||||
|
# 2. Login (FlutterFire uses the Firebase CLI session)
|
||||||
|
firebase login
|
||||||
|
|
||||||
|
# 3. Register apps + download config (android, ios, web)
|
||||||
|
cd ~/cyberhybridhub.com
|
||||||
|
flutterfire configure \
|
||||||
|
--project=YOUR_FIREBASE_PROJECT_ID \
|
||||||
|
--platforms=android,ios,web \
|
||||||
|
--android-package-name=com.cyberhybridhub.cyberhybridhub \
|
||||||
|
--ios-bundle-id=com.cyberhybridhub.cyberhybridhub \
|
||||||
|
--out=lib/firebase_options.dart
|
||||||
|
|
||||||
|
# 4. Debug SHA-1 (Android Google Sign-In)
|
||||||
|
./scripts/get-android-sha.sh
|
||||||
|
firebase apps:list --project=YOUR_FIREBASE_PROJECT_ID
|
||||||
|
firebase apps:android:sha:create ANDROID_APP_ID YOUR_SHA1 --project=YOUR_FIREBASE_PROJECT_ID
|
||||||
|
flutterfire configure --project=YOUR_FIREBASE_PROJECT_ID --platforms=android,ios,web --yes
|
||||||
|
|
||||||
|
# 5. Enable Google provider (browser — required)
|
||||||
|
# https://console.firebase.google.com/ → Authentication → Sign-in method → Google → Enable
|
||||||
|
|
||||||
|
# 6. Run
|
||||||
|
flutter pub get && flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
`~/.bashrc` includes `~/.pub-cache/bin` (for `flutterfire`) and `CYBERHYBRIDHUB_ROOT`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux desktop development (`flutter run -d linux`)
|
||||||
|
|
||||||
|
Official FlutterFire does not register native plugins on Linux, so this app uses:
|
||||||
|
|
||||||
|
- `google_sign_in_all_platforms` (system browser OAuth)
|
||||||
|
- Firebase Auth REST (`signInWithIdp`) with web API key from `lib/firebase_options.dart`
|
||||||
|
|
||||||
|
### One-time Linux OAuth setup
|
||||||
|
|
||||||
|
1. [Google Cloud Console](https://console.cloud.google.com/) → project **cyberhybridhub** → **APIs & Services** → **Credentials**
|
||||||
|
2. Open your **Web application** OAuth 2.0 client (or create one)
|
||||||
|
3. Add authorized redirect URI: `http://127.0.0.1`
|
||||||
|
4. Copy **Client ID** and **Client secret** into `lib/config/auth_config.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const String? googleWebOAuthClientId = 'YOUR_CLIENT_ID.apps.googleusercontent.com';
|
||||||
|
const String? googleOAuthDesktopClientSecret = 'YOUR_CLIENT_SECRET';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web browser (`flutter run -d web-server` or Chrome)
|
||||||
|
|
||||||
|
Uses the same `googleWebOAuthClientId`. In Google Cloud, on that Web OAuth client, add **Authorized JavaScript origins**:
|
||||||
|
|
||||||
|
- `http://localhost`
|
||||||
|
- `http://127.0.0.1`
|
||||||
|
|
||||||
|
(Uncomment the `google-signin-client_id` meta tag in `web/index.html` only if you prefer HTML config over Dart.)
|
||||||
|
|
||||||
|
5. Enable Google sign-in in Firebase Console (if not already)
|
||||||
|
6. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run -d linux
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual checklist
|
||||||
|
|
||||||
|
Complete these steps to replace placeholder config files and enable Google authentication.
|
||||||
|
|
||||||
|
App identifiers used by this project:
|
||||||
|
|
||||||
|
| Platform | Identifier |
|
||||||
|
|----------|------------|
|
||||||
|
| Android package | `com.cyberhybridhub.cyberhybridhub` |
|
||||||
|
| iOS bundle ID | `com.cyberhybridhub.cyberhybridhub` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Create a Firebase project
|
||||||
|
|
||||||
|
- [ ] Open [Firebase Console](https://console.firebase.google.com/).
|
||||||
|
- [ ] Create a project (or select an existing one).
|
||||||
|
- [ ] Enable **Google Analytics** only if you need it (optional for auth).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Register the Flutter app in Firebase
|
||||||
|
|
||||||
|
- [ ] In Firebase Console → **Project settings** → **Your apps**, add:
|
||||||
|
- [ ] **Android** app with package name `com.cyberhybridhub.cyberhybridhub`
|
||||||
|
- [ ] **iOS** app with bundle ID `com.cyberhybridhub.cyberhybridhub`
|
||||||
|
- [ ] **Web** app (required for the web OAuth client used by Google Sign-In on mobile)
|
||||||
|
- [ ] Download config files when prompted:
|
||||||
|
- [ ] `google-services.json` → replace `android/app/google-services.json`
|
||||||
|
- [ ] `GoogleService-Info.plist` → replace `ios/Runner/GoogleService-Info.plist`
|
||||||
|
|
||||||
|
### Recommended: FlutterFire CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart pub global activate flutterfire_cli
|
||||||
|
flutterfire configure
|
||||||
|
```
|
||||||
|
|
||||||
|
This regenerates `lib/firebase_options.dart` and downloads the platform config files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Enable Google sign-in in Firebase
|
||||||
|
|
||||||
|
- [ ] Firebase Console → **Build** → **Authentication** → **Sign-in method**
|
||||||
|
- [ ] Enable **Google**
|
||||||
|
- [ ] Set a support email and save
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Google Cloud Console (OAuth & credentials)
|
||||||
|
|
||||||
|
Firebase links to a Google Cloud project. Open it from Firebase → **Project settings** → **General** → **Google Cloud Platform resource location** → link to GCP, or go to [Google Cloud Console](https://console.cloud.google.com/) and select the same project.
|
||||||
|
|
||||||
|
### 4.1 OAuth consent screen
|
||||||
|
|
||||||
|
- [ ] **APIs & Services** → **OAuth consent screen**
|
||||||
|
- [ ] Choose **External** (or Internal for Workspace-only)
|
||||||
|
- [ ] Fill app name, user support email, developer contact
|
||||||
|
- [ ] Add scopes if needed (default `email`, `profile`, `openid` are usually enough)
|
||||||
|
- [ ] Add test users while the app is in **Testing** mode
|
||||||
|
|
||||||
|
### 4.2 OAuth 2.0 Client IDs
|
||||||
|
|
||||||
|
- [ ] **APIs & Services** → **Credentials**
|
||||||
|
- [ ] Confirm these clients exist (Firebase often creates them automatically):
|
||||||
|
|
||||||
|
| Client type | Purpose |
|
||||||
|
|-------------|---------|
|
||||||
|
| **Web application** | `serverClientId` / ID token for Firebase Auth on Android |
|
||||||
|
| **Android** | Package name + SHA-1 certificate fingerprints |
|
||||||
|
| **iOS** | Bundle ID |
|
||||||
|
|
||||||
|
#### Android SHA-1 fingerprints
|
||||||
|
|
||||||
|
Debug keystore (local development):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
|
||||||
|
```
|
||||||
|
|
||||||
|
Release keystore (production):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v -keystore /path/to/your-release.keystore -alias your-alias
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add **SHA-1** (and **SHA-256** if offered) to the Android OAuth client in Google Cloud Console
|
||||||
|
- [ ] Re-download `google-services.json` after adding fingerprints
|
||||||
|
|
||||||
|
#### iOS URL scheme
|
||||||
|
|
||||||
|
- [ ] Open `ios/Runner/GoogleService-Info.plist` and copy `REVERSED_CLIENT_ID`
|
||||||
|
- [ ] In Xcode (or `ios/Runner/Info.plist`), add a URL type:
|
||||||
|
- **URL Schemes**: value of `REVERSED_CLIENT_ID`
|
||||||
|
- Example: `com.googleusercontent.apps.123456789-abcdef`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. App code configuration
|
||||||
|
|
||||||
|
- [ ] Replace placeholders in `lib/firebase_options.dart` (or run `flutterfire configure`)
|
||||||
|
- [ ] If sign-in fails on Android with a `serverClientId` error, set the Web client ID in `lib/config/auth_config.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const String? googleSignInServerClientId = 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com';
|
||||||
|
```
|
||||||
|
|
||||||
|
Find the Web client ID in Firebase → Project settings → Your apps → Web app, or in `google-services.json` under `oauth_client` with `"client_type": 3`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Tap **Continue with Google** on the login screen
|
||||||
|
- [ ] Complete Google account selection
|
||||||
|
- [ ] Confirm the home screen shows your name and email
|
||||||
|
- [ ] Sign out and sign in again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely fix |
|
||||||
|
|---------|------------|
|
||||||
|
| `clientConfigurationError` | Wrong package/bundle ID, missing SHA-1, or invalid `google-services.json` |
|
||||||
|
| Sign-in cancels immediately after account pick | Missing web OAuth client in `google-services.json` or wrong SHA-1 |
|
||||||
|
| `serverClientId must be provided on Android` | Add Web app in Firebase, re-download `google-services.json`, or set `googleSignInServerClientId` |
|
||||||
|
| iOS sign-in fails after Google UI | Add `REVERSED_CLIENT_ID` URL scheme to `Info.plist` |
|
||||||
|
| `REPLACE_ME` / invalid API key at runtime | Run `flutterfire configure` or paste real values from Firebase |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [FlutterFire setup](https://firebase.flutter.dev/docs/overview)
|
||||||
|
- [Firebase Auth – Google](https://firebase.google.com/docs/auth/flutter/federated-auth#google)
|
||||||
|
- [google_sign_in plugin](https://pub.dev/packages/google_sign_in)
|
||||||
28
analysis_options.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
android/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
46
android/app/build.gradle.kts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.cyberhybridhub.cyberhybridhub"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.cyberhybridhub.cyberhybridhub"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
29
android/app/google-services.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "155018389717",
|
||||||
|
"project_id": "cyberhybridhub",
|
||||||
|
"storage_bucket": "cyberhybridhub.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:155018389717:android:90887875660a26509ada5c",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.cyberhybridhub.cyberhybridhub"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyA71SkNLrv3tyJQW2DCfe2vFcu1c2opDX8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="cyberhybridhub"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.cyberhybridhub.cyberhybridhub
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
android/build.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
6
android/gradle.properties
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
# This newDsl flag was added by the Flutter template
|
||||||
|
android.newDsl=false
|
||||||
|
# This builtInKotlin flag was added by the Flutter template
|
||||||
|
android.builtInKotlin=false
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||||
27
android/settings.gradle.kts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "9.0.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
34
firebase.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"providers": {
|
||||||
|
"googleSignIn": {
|
||||||
|
"oAuthBrandDisplayName": "Cyber Hybrid Hub",
|
||||||
|
"supportEmail": "support@cyberhybridhub.firebaseapp.com",
|
||||||
|
"authorizedRedirectUris": [
|
||||||
|
"http://localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flutter": {
|
||||||
|
"platforms": {
|
||||||
|
"android": {
|
||||||
|
"default": {
|
||||||
|
"projectId": "cyberhybridhub",
|
||||||
|
"appId": "1:155018389717:android:90887875660a26509ada5c",
|
||||||
|
"fileOutput": "android/app/google-services.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dart": {
|
||||||
|
"lib/firebase_options.dart": {
|
||||||
|
"projectId": "cyberhybridhub",
|
||||||
|
"configurations": {
|
||||||
|
"android": "1:155018389717:android:90887875660a26509ada5c",
|
||||||
|
"ios": "1:155018389717:ios:102445b8ec5fde749ada5c",
|
||||||
|
"web": "1:155018389717:web:afb61503456673019ada5c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ios/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
24
ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
1
ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
644
ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,644 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cyberhybridhub.cyberhybridhub;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
119
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
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.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
34
ios/Runner/GoogleService-Info.plist
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>REPLACE_ME.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.REPLACE_ME</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>REPLACE_ME</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>000000000000</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.cyberhybridhub.cyberhybridhub</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>REPLACE_ME</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>REPLACE_ME.appspot.com</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:000000000000:ios:0000000000000000000000</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
70
ios/Runner/Info.plist
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Cyberhybridhub</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>cyberhybridhub</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
6
ios/Runner/SceneDelegate.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
lib/bootstrap.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
||||||
|
import 'firebase_options.dart';
|
||||||
|
import 'repositories/user_profile_repository.dart';
|
||||||
|
import 'services/auth_service.dart';
|
||||||
|
|
||||||
|
/// Initializes Firebase (non-Linux), auth, and profile sync storage.
|
||||||
|
Future<void> bootstrap() async {
|
||||||
|
if (!AuthService.isLinuxDesktop) {
|
||||||
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await AuthService.instance.initialize();
|
||||||
|
await UserProfileRepository.instance.initialize();
|
||||||
|
}
|
||||||
8
lib/config/api_config.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// Base URL for the Cyber Hybrid Hub API (Postgres-backed).
|
||||||
|
///
|
||||||
|
/// Override at build/run time:
|
||||||
|
/// flutter run --dart-define=API_BASE_URL=http://localhost:3000
|
||||||
|
const String apiBaseUrl = String.fromEnvironment(
|
||||||
|
'API_BASE_URL',
|
||||||
|
defaultValue: 'http://localhost:3000',
|
||||||
|
);
|
||||||
25
lib/config/auth_config.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/// Web OAuth 2.0 client ID (Google Cloud → Credentials → Web application).
|
||||||
|
///
|
||||||
|
/// Used for:
|
||||||
|
/// - **Android**: `serverClientId` when not in `google-services.json`
|
||||||
|
/// - **Linux desktop**: `google_sign_in_all_platforms` clientId
|
||||||
|
///
|
||||||
|
/// **Web (required for Firefox / strict privacy):** `clientId` for the
|
||||||
|
/// Google Identity Services fallback when Firebase popup sign-in fails.
|
||||||
|
/// Popup is tried first; redirect is not used (sessionStorage breaks under ETP).
|
||||||
|
///
|
||||||
|
/// Find it: Firebase Console → Project settings → Your apps → Web app,
|
||||||
|
/// or Authentication → Sign-in method → Google → Web SDK configuration.
|
||||||
|
///
|
||||||
|
/// Also add authorized JavaScript origins in Google Cloud for web dev:
|
||||||
|
/// `http://localhost:8080` (see [web_dev_config.yaml] in the project root).
|
||||||
|
const String? googleWebOAuthClientId =
|
||||||
|
'155018389717-cers33qaak01urmf4sa04s85h7kcocpt.apps.googleusercontent.com';
|
||||||
|
|
||||||
|
/// @deprecated Use [googleWebOAuthClientId].
|
||||||
|
const String? googleSignInServerClientId = googleWebOAuthClientId;
|
||||||
|
|
||||||
|
/// Linux desktop OAuth client secret (same Web client as above).
|
||||||
|
const String? googleOAuthDesktopClientId = googleWebOAuthClientId;
|
||||||
|
|
||||||
|
const String? googleOAuthDesktopClientSecret = null;
|
||||||
71
lib/data/local/app_database.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
|
||||||
|
import 'tables.dart';
|
||||||
|
|
||||||
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(tables: <Type>[UserProfileRows, SyncOutboxRows])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(name: 'cyberhybridhub');
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<UserProfileRow?> watchProfile(String firebaseUid) {
|
||||||
|
return (select(userProfileRows)
|
||||||
|
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
|
||||||
|
.watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserProfileRow?> getProfile(String firebaseUid) {
|
||||||
|
return (select(userProfileRows)
|
||||||
|
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> upsertProfile(UserProfileRowsCompanion row) {
|
||||||
|
return into(userProfileRows).insertOnConflictUpdate(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteProfile(String firebaseUid) {
|
||||||
|
return (delete(userProfileRows)
|
||||||
|
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllProfiles() {
|
||||||
|
return delete(userProfileRows).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SyncOutboxRow>> pendingOutbox(String firebaseUid) {
|
||||||
|
return (select(syncOutboxRows)
|
||||||
|
..where((SyncOutboxRows t) => t.firebaseUid.equals(firebaseUid))
|
||||||
|
..orderBy(<OrderClauseGenerator<SyncOutboxRows>>[
|
||||||
|
(SyncOutboxRows t) => OrderingTerm(expression: t.createdAt),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> enqueueOutbox(SyncOutboxRowsCompanion row) {
|
||||||
|
return into(syncOutboxRows).insert(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteOutboxEntry(int id) {
|
||||||
|
return (delete(syncOutboxRows)
|
||||||
|
..where((SyncOutboxRows t) => t.id.equals(id)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearOutbox(String firebaseUid) {
|
||||||
|
return (delete(syncOutboxRows)
|
||||||
|
..where((SyncOutboxRows t) => t.firebaseUid.equals(firebaseUid)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllOutbox() => delete(syncOutboxRows).go();
|
||||||
|
}
|
||||||
1535
lib/data/local/app_database.g.dart
Normal file
26
lib/data/local/tables.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
class UserProfileRows extends Table {
|
||||||
|
TextColumn get firebaseUid => text()();
|
||||||
|
TextColumn get email => text().nullable()();
|
||||||
|
TextColumn get displayName => text().nullable()();
|
||||||
|
TextColumn get photoUrl => text().nullable()();
|
||||||
|
TextColumn get locale => text().withDefault(const Constant('en'))();
|
||||||
|
TextColumn get timezone => text().nullable()();
|
||||||
|
BoolColumn get onboardingCompleted =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
IntColumn get revision => integer().withDefault(const Constant(1))();
|
||||||
|
DateTimeColumn get updatedAt => dateTime()();
|
||||||
|
DateTimeColumn get lastSyncedAt => dateTime().nullable()();
|
||||||
|
BoolColumn get dirty => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column<Object>> get primaryKey => <Column<Object>>{firebaseUid};
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncOutboxRows extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get firebaseUid => text()();
|
||||||
|
TextColumn get payloadJson => text()();
|
||||||
|
DateTimeColumn get createdAt => dateTime()();
|
||||||
|
}
|
||||||
70
lib/data/local/user_profile_local_store.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import '../../models/user_profile.dart';
|
||||||
|
import 'app_database.dart';
|
||||||
|
import 'user_profile_mapper.dart';
|
||||||
|
|
||||||
|
/// Drift-backed local profile storage (mobile and desktop native).
|
||||||
|
class UserProfileLocalStore {
|
||||||
|
UserProfileLocalStore(this._db);
|
||||||
|
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
Stream<UserProfile?> watchProfile(String firebaseUid) {
|
||||||
|
return _db.watchProfile(firebaseUid).map((UserProfileRow? row) {
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return profileFromRow(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserProfile?> getProfile(String firebaseUid) async {
|
||||||
|
final UserProfileRow? row = await _db.getProfile(firebaseUid);
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return profileFromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveProfile(UserProfile profile, {required bool dirty}) async {
|
||||||
|
await _db.upsertProfile(
|
||||||
|
companionFromProfile(profile.copyWith(dirty: dirty)),
|
||||||
|
);
|
||||||
|
if (dirty) {
|
||||||
|
await _db.enqueueOutbox(
|
||||||
|
SyncOutboxRowsCompanion.insert(
|
||||||
|
firebaseUid: profile.firebaseUid,
|
||||||
|
payloadJson: encodeProfilePayload(profile),
|
||||||
|
createdAt: DateTime.now().toUtc(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<UserProfile>> pendingOutboxProfiles(String firebaseUid) async {
|
||||||
|
final List<SyncOutboxRow> rows = await _db.pendingOutbox(firebaseUid);
|
||||||
|
return rows
|
||||||
|
.map((SyncOutboxRow row) => decodeProfilePayload(row.payloadJson))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearOutbox(String firebaseUid) async {
|
||||||
|
await _db.clearOutbox(firebaseUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteOutboxEntries(String firebaseUid) async {
|
||||||
|
final List<SyncOutboxRow> rows = await _db.pendingOutbox(firebaseUid);
|
||||||
|
for (final SyncOutboxRow row in rows) {
|
||||||
|
await _db.deleteOutboxEntry(row.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearLocal(String firebaseUid) async {
|
||||||
|
await _db.clearOutbox(firebaseUid);
|
||||||
|
await _db.deleteProfile(firebaseUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAll() async {
|
||||||
|
await _db.clearAllProfiles();
|
||||||
|
await _db.clearAllOutbox();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/data/local/user_profile_mapper.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
import '../../models/user_profile.dart';
|
||||||
|
import 'app_database.dart';
|
||||||
|
|
||||||
|
UserProfile profileFromRow(UserProfileRow row) {
|
||||||
|
return UserProfile(
|
||||||
|
firebaseUid: row.firebaseUid,
|
||||||
|
email: row.email,
|
||||||
|
displayName: row.displayName,
|
||||||
|
photoUrl: row.photoUrl,
|
||||||
|
locale: row.locale,
|
||||||
|
timezone: row.timezone,
|
||||||
|
onboardingCompleted: row.onboardingCompleted,
|
||||||
|
revision: row.revision,
|
||||||
|
updatedAt: row.updatedAt.toUtc(),
|
||||||
|
lastSyncedAt: row.lastSyncedAt?.toUtc(),
|
||||||
|
dirty: row.dirty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfileRowsCompanion companionFromProfile(UserProfile profile) {
|
||||||
|
return UserProfileRowsCompanion(
|
||||||
|
firebaseUid: Value<String>(profile.firebaseUid),
|
||||||
|
email: Value<String?>(profile.email),
|
||||||
|
displayName: Value<String?>(profile.displayName),
|
||||||
|
photoUrl: Value<String?>(profile.photoUrl),
|
||||||
|
locale: Value<String>(profile.locale),
|
||||||
|
timezone: Value<String?>(profile.timezone),
|
||||||
|
onboardingCompleted: Value<bool>(profile.onboardingCompleted),
|
||||||
|
revision: Value<int>(profile.revision),
|
||||||
|
updatedAt: Value<DateTime>(profile.updatedAt.toUtc()),
|
||||||
|
lastSyncedAt: Value<DateTime?>(profile.lastSyncedAt?.toUtc()),
|
||||||
|
dirty: Value<bool>(profile.dirty),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String encodeProfilePayload(UserProfile profile) {
|
||||||
|
return jsonEncode(profile.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfile decodeProfilePayload(String json) {
|
||||||
|
return UserProfile.fromJson(
|
||||||
|
jsonDecode(json) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
19
lib/data/remote/profile_api_exception.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import '../../models/user_profile.dart';
|
||||||
|
|
||||||
|
class ProfileNotFoundException implements Exception {}
|
||||||
|
|
||||||
|
class ProfileConflictException implements Exception {
|
||||||
|
ProfileConflictException(this.serverProfile);
|
||||||
|
|
||||||
|
final UserProfile serverProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileApiException implements Exception {
|
||||||
|
ProfileApiException(this.statusCode, this.body);
|
||||||
|
|
||||||
|
final int statusCode;
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ProfileApiException($statusCode): $body';
|
||||||
|
}
|
||||||
90
lib/data/remote/user_profile_remote_store.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../../config/api_config.dart';
|
||||||
|
import '../../models/user_profile.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
import 'profile_api_exception.dart';
|
||||||
|
|
||||||
|
/// HTTP client for Postgres-backed profile API.
|
||||||
|
class UserProfileRemoteStore {
|
||||||
|
UserProfileRemoteStore({http.Client? client}) : _client = client ?? http.Client();
|
||||||
|
|
||||||
|
final http.Client _client;
|
||||||
|
|
||||||
|
Future<UserProfile?> fetchProfile() async {
|
||||||
|
final String? token = await AuthService.instance.getIdToken();
|
||||||
|
if (token == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final http.Response response = await _client.get(
|
||||||
|
Uri.parse('$apiBaseUrl/v1/me/profile'),
|
||||||
|
headers: _authHeaders(token),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw ProfileApiException(response.statusCode, response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _profileFromApi(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserProfile> pushProfile(UserProfile profile) async {
|
||||||
|
final String? token = await AuthService.instance.getIdToken();
|
||||||
|
if (token == null) {
|
||||||
|
throw StateError('Not signed in');
|
||||||
|
}
|
||||||
|
|
||||||
|
final http.Response response = await _client.put(
|
||||||
|
Uri.parse('$apiBaseUrl/v1/me/profile'),
|
||||||
|
headers: _authHeaders(token),
|
||||||
|
body: jsonEncode(profile.toApiJson()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 409) {
|
||||||
|
final Map<String, dynamic> body =
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
throw ProfileConflictException(
|
||||||
|
_profileFromApi(body['profile'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw ProfileApiException(response.statusCode, response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _profileFromApi(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> _authHeaders(String token) {
|
||||||
|
return <String, String>{
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfile _profileFromApi(Map<String, dynamic> json) {
|
||||||
|
return UserProfile(
|
||||||
|
firebaseUid: json['firebaseUid'] as String,
|
||||||
|
email: json['email'] as String?,
|
||||||
|
displayName: json['displayName'] as String?,
|
||||||
|
photoUrl: json['photoUrl'] as String?,
|
||||||
|
locale: json['locale'] as String? ?? 'en',
|
||||||
|
timezone: json['timezone'] as String?,
|
||||||
|
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
|
||||||
|
revision: (json['revision'] as num).toInt(),
|
||||||
|
updatedAt: DateTime.parse(json['updatedAt'] as String).toUtc(),
|
||||||
|
lastSyncedAt: DateTime.now().toUtc(),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/data/sync/connectivity_service.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
|
/// Observes network reachability and exposes online/offline transitions.
|
||||||
|
class ConnectivityService {
|
||||||
|
ConnectivityService({Connectivity? connectivity})
|
||||||
|
: _connectivity = connectivity ?? Connectivity();
|
||||||
|
|
||||||
|
final Connectivity _connectivity;
|
||||||
|
final StreamController<bool> _onlineController =
|
||||||
|
StreamController<bool>.broadcast();
|
||||||
|
|
||||||
|
StreamSubscription<List<ConnectivityResult>>? _subscription;
|
||||||
|
bool _isOnline = true;
|
||||||
|
|
||||||
|
Stream<bool> get onlineChanges => _onlineController.stream;
|
||||||
|
|
||||||
|
bool get isOnline => _isOnline;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
_isOnline = true;
|
||||||
|
_onlineController.add(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ConnectivityResult> results = await _connectivity
|
||||||
|
.checkConnectivity();
|
||||||
|
_emitOnline(_resultsOnline(results));
|
||||||
|
|
||||||
|
_subscription = _connectivity.onConnectivityChanged.listen(
|
||||||
|
(List<ConnectivityResult> results) => _emitOnline(_resultsOnline(results)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_onlineController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _resultsOnline(List<ConnectivityResult> results) {
|
||||||
|
if (results.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return results.any(
|
||||||
|
(ConnectivityResult r) =>
|
||||||
|
r == ConnectivityResult.mobile ||
|
||||||
|
r == ConnectivityResult.wifi ||
|
||||||
|
r == ConnectivityResult.ethernet ||
|
||||||
|
r == ConnectivityResult.vpn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitOnline(bool online) {
|
||||||
|
if (_isOnline == online) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isOnline = online;
|
||||||
|
_onlineController.add(online);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
lib/data/sync/profile_sync_coordinator.dart
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
|
import '../../models/sync_result.dart';
|
||||||
|
import '../../models/user_profile.dart';
|
||||||
|
import '../local/user_profile_local_store.dart';
|
||||||
|
import '../remote/profile_api_exception.dart';
|
||||||
|
import '../remote/user_profile_remote_store.dart';
|
||||||
|
import 'connectivity_service.dart';
|
||||||
|
|
||||||
|
/// Pushes local changes and pulls server state when online.
|
||||||
|
class ProfileSyncCoordinator {
|
||||||
|
ProfileSyncCoordinator({
|
||||||
|
required UserProfileLocalStore? local,
|
||||||
|
required UserProfileRemoteStore remote,
|
||||||
|
required ConnectivityService connectivity,
|
||||||
|
}) : _local = local,
|
||||||
|
_remote = remote,
|
||||||
|
_connectivity = connectivity;
|
||||||
|
|
||||||
|
final UserProfileLocalStore? _local;
|
||||||
|
final UserProfileRemoteStore _remote;
|
||||||
|
final ConnectivityService _connectivity;
|
||||||
|
|
||||||
|
bool get hasLocalStore => _local != null;
|
||||||
|
|
||||||
|
Future<SyncResult> sync(String firebaseUid) async {
|
||||||
|
if (!kIsWeb && !_connectivity.isOnline) {
|
||||||
|
return const SyncResult.offline();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (_local == null) {
|
||||||
|
return const SyncResult.success();
|
||||||
|
}
|
||||||
|
return _syncWithLocal(firebaseUid);
|
||||||
|
} on ProfileApiException catch (e) {
|
||||||
|
return SyncResult.error(e.toString());
|
||||||
|
} catch (e) {
|
||||||
|
return SyncResult.error(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SyncResult> _syncWithLocal(String firebaseUid) async {
|
||||||
|
final UserProfileLocalStore local = _local!;
|
||||||
|
|
||||||
|
final List<UserProfile> pending = await local.pendingOutboxProfiles(
|
||||||
|
firebaseUid,
|
||||||
|
);
|
||||||
|
for (final UserProfile profile in pending) {
|
||||||
|
try {
|
||||||
|
final UserProfile saved = await _remote.pushProfile(profile);
|
||||||
|
await local.saveProfile(
|
||||||
|
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
} on ProfileConflictException catch (e) {
|
||||||
|
await local.saveProfile(
|
||||||
|
e.serverProfile.copyWith(
|
||||||
|
dirty: false,
|
||||||
|
lastSyncedAt: DateTime.now().toUtc(),
|
||||||
|
),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
await local.clearOutbox(firebaseUid);
|
||||||
|
return const SyncResult.conflictResolved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pending.isNotEmpty) {
|
||||||
|
await local.clearOutbox(firebaseUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
final UserProfile? server = await _remote.fetchProfile();
|
||||||
|
if (server == null) {
|
||||||
|
final UserProfile? localProfile = await local.getProfile(firebaseUid);
|
||||||
|
if (localProfile != null && localProfile.dirty) {
|
||||||
|
final UserProfile saved = await _remote.pushProfile(localProfile);
|
||||||
|
await local.saveProfile(
|
||||||
|
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SyncResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
final UserProfile? localProfile = await local.getProfile(firebaseUid);
|
||||||
|
if (localProfile == null ||
|
||||||
|
server.revision > localProfile.revision ||
|
||||||
|
(server.revision == localProfile.revision &&
|
||||||
|
server.updatedAt.isAfter(localProfile.updatedAt))) {
|
||||||
|
await local.saveProfile(
|
||||||
|
server.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
} else if (localProfile.dirty) {
|
||||||
|
try {
|
||||||
|
final UserProfile saved = await _remote.pushProfile(localProfile);
|
||||||
|
await local.saveProfile(
|
||||||
|
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
} on ProfileConflictException catch (e) {
|
||||||
|
await local.saveProfile(
|
||||||
|
e.serverProfile.copyWith(
|
||||||
|
dirty: false,
|
||||||
|
lastSyncedAt: DateTime.now().toUtc(),
|
||||||
|
),
|
||||||
|
dirty: false,
|
||||||
|
);
|
||||||
|
return const SyncResult.conflictResolved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SyncResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/firebase_options.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// File generated by FlutterFire CLI.
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||||
|
|
||||||
|
/// Placeholder Firebase options.
|
||||||
|
///
|
||||||
|
/// Replace this file by running:
|
||||||
|
/// dart pub global activate flutterfire_cli
|
||||||
|
/// flutterfire configure
|
||||||
|
class DefaultFirebaseOptions {
|
||||||
|
static FirebaseOptions get currentPlatform {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return web;
|
||||||
|
}
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return android;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return ios;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return macos;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
return linux;
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return web;
|
||||||
|
default:
|
||||||
|
throw UnsupportedError(
|
||||||
|
'DefaultFirebaseOptions are not configured for this platform.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for Linux desktop dev (Firebase REST + browser OAuth).
|
||||||
|
static const FirebaseOptions linux = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyDbYBh6nO-UasqwM-dkVVJAvw5wMhRTsIs',
|
||||||
|
appId: '1:155018389717:web:afb61503456673019ada5c',
|
||||||
|
messagingSenderId: '155018389717',
|
||||||
|
projectId: 'cyberhybridhub',
|
||||||
|
authDomain: 'cyberhybridhub.firebaseapp.com',
|
||||||
|
storageBucket: 'cyberhybridhub.firebasestorage.app',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions web = linux;
|
||||||
|
|
||||||
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyA71SkNLrv3tyJQW2DCfe2vFcu1c2opDX8',
|
||||||
|
appId: '1:155018389717:android:90887875660a26509ada5c',
|
||||||
|
messagingSenderId: '155018389717',
|
||||||
|
projectId: 'cyberhybridhub',
|
||||||
|
storageBucket: 'cyberhybridhub.firebasestorage.app',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
|
apiKey: 'AIzaSyAHqjmp-vC8n_2EClQM2ch33jL_yIGuQLE',
|
||||||
|
appId: '1:155018389717:ios:102445b8ec5fde749ada5c',
|
||||||
|
messagingSenderId: '155018389717',
|
||||||
|
projectId: 'cyberhybridhub',
|
||||||
|
storageBucket: 'cyberhybridhub.firebasestorage.app',
|
||||||
|
iosBundleId: 'com.cyberhybridhub.cyberhybridhub',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const FirebaseOptions macos = ios;
|
||||||
|
}
|
||||||
62
lib/main.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'bootstrap.dart';
|
||||||
|
import 'models/app_user.dart';
|
||||||
|
import 'screens/home_screen.dart';
|
||||||
|
import 'screens/landing_screen.dart';
|
||||||
|
import 'services/auth_service.dart';
|
||||||
|
import 'theme/app_theme.dart';
|
||||||
|
import 'widgets/profile_session.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await bootstrap();
|
||||||
|
runApp(const CyberHybridHubApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class CyberHybridHubApp extends StatelessWidget {
|
||||||
|
const CyberHybridHubApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Cyber Hybrid Hub',
|
||||||
|
theme: buildAppTheme(),
|
||||||
|
home: const AuthGate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthGate extends StatelessWidget {
|
||||||
|
const AuthGate({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<AppUser?>(
|
||||||
|
stream: AuthService.instance.authStateChanges,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<AppUser?> snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColors.accent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AppUser? user = snapshot.data;
|
||||||
|
if (user == null) {
|
||||||
|
return const LandingScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProfileSession(
|
||||||
|
user: user,
|
||||||
|
builder: (context, profile, syncStatus) => HomeScreen(
|
||||||
|
user: user,
|
||||||
|
profile: profile,
|
||||||
|
syncStatus: syncStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/models/app_user.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/// App-level user model used across mobile, web, and Linux desktop.
|
||||||
|
class AppUser {
|
||||||
|
const AppUser({
|
||||||
|
required this.uid,
|
||||||
|
this.email,
|
||||||
|
this.displayName,
|
||||||
|
this.photoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String uid;
|
||||||
|
final String? email;
|
||||||
|
final String? displayName;
|
||||||
|
final String? photoUrl;
|
||||||
|
}
|
||||||
26
lib/models/sync_result.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/// Result of a profile sync attempt.
|
||||||
|
enum SyncResultKind { success, offline, conflictResolved, error }
|
||||||
|
|
||||||
|
class SyncResult {
|
||||||
|
const SyncResult._(this.kind, {this.message});
|
||||||
|
|
||||||
|
const SyncResult.success() : this._(SyncResultKind.success);
|
||||||
|
|
||||||
|
const SyncResult.offline() : this._(SyncResultKind.offline);
|
||||||
|
|
||||||
|
const SyncResult.conflictResolved()
|
||||||
|
: this._(SyncResultKind.conflictResolved);
|
||||||
|
|
||||||
|
const SyncResult.error(String message)
|
||||||
|
: this._(SyncResultKind.error, message: message);
|
||||||
|
|
||||||
|
final SyncResultKind kind;
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
bool get isSuccess =>
|
||||||
|
kind == SyncResultKind.success ||
|
||||||
|
kind == SyncResultKind.conflictResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI-facing sync state from [UserProfileRepository].
|
||||||
|
enum ProfileSyncStatus { idle, syncing, synced, offline, error }
|
||||||
123
lib/models/user_profile.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/// App-owned user profile synced between local Drift storage and Postgres.
|
||||||
|
class UserProfile {
|
||||||
|
const UserProfile({
|
||||||
|
required this.firebaseUid,
|
||||||
|
this.email,
|
||||||
|
this.displayName,
|
||||||
|
this.photoUrl,
|
||||||
|
this.locale = 'en',
|
||||||
|
this.timezone,
|
||||||
|
this.onboardingCompleted = false,
|
||||||
|
this.revision = 1,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.lastSyncedAt,
|
||||||
|
this.dirty = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String firebaseUid;
|
||||||
|
final String? email;
|
||||||
|
final String? displayName;
|
||||||
|
final String? photoUrl;
|
||||||
|
final String locale;
|
||||||
|
final String? timezone;
|
||||||
|
final bool onboardingCompleted;
|
||||||
|
final int revision;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final DateTime? lastSyncedAt;
|
||||||
|
final bool dirty;
|
||||||
|
|
||||||
|
UserProfile copyWith({
|
||||||
|
String? email,
|
||||||
|
String? displayName,
|
||||||
|
String? photoUrl,
|
||||||
|
String? locale,
|
||||||
|
String? timezone,
|
||||||
|
bool? onboardingCompleted,
|
||||||
|
int? revision,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
DateTime? lastSyncedAt,
|
||||||
|
bool? dirty,
|
||||||
|
bool clearLastSyncedAt = false,
|
||||||
|
}) {
|
||||||
|
return UserProfile(
|
||||||
|
firebaseUid: firebaseUid,
|
||||||
|
email: email ?? this.email,
|
||||||
|
displayName: displayName ?? this.displayName,
|
||||||
|
photoUrl: photoUrl ?? this.photoUrl,
|
||||||
|
locale: locale ?? this.locale,
|
||||||
|
timezone: timezone ?? this.timezone,
|
||||||
|
onboardingCompleted: onboardingCompleted ?? this.onboardingCompleted,
|
||||||
|
revision: revision ?? this.revision,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
lastSyncedAt: clearLastSyncedAt
|
||||||
|
? null
|
||||||
|
: (lastSyncedAt ?? this.lastSyncedAt),
|
||||||
|
dirty: dirty ?? this.dirty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UserProfile.fromAuth({
|
||||||
|
required String firebaseUid,
|
||||||
|
String? email,
|
||||||
|
String? displayName,
|
||||||
|
String? photoUrl,
|
||||||
|
}) {
|
||||||
|
return UserProfile(
|
||||||
|
firebaseUid: firebaseUid,
|
||||||
|
email: email,
|
||||||
|
displayName: displayName,
|
||||||
|
photoUrl: photoUrl,
|
||||||
|
updatedAt: DateTime.now().toUtc(),
|
||||||
|
dirty: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UserProfile.fromJson(Map<String, dynamic> json) {
|
||||||
|
return UserProfile(
|
||||||
|
firebaseUid: json['firebaseUid'] as String,
|
||||||
|
email: json['email'] as String?,
|
||||||
|
displayName: json['displayName'] as String?,
|
||||||
|
photoUrl: json['photoUrl'] as String?,
|
||||||
|
locale: json['locale'] as String? ?? 'en',
|
||||||
|
timezone: json['timezone'] as String?,
|
||||||
|
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
|
||||||
|
revision: (json['revision'] as num?)?.toInt() ?? 1,
|
||||||
|
updatedAt: DateTime.parse(json['updatedAt'] as String).toUtc(),
|
||||||
|
lastSyncedAt: json['lastSyncedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['lastSyncedAt'] as String).toUtc(),
|
||||||
|
dirty: json['dirty'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'firebaseUid': firebaseUid,
|
||||||
|
'email': email,
|
||||||
|
'displayName': displayName,
|
||||||
|
'photoUrl': photoUrl,
|
||||||
|
'locale': locale,
|
||||||
|
'timezone': timezone,
|
||||||
|
'onboardingCompleted': onboardingCompleted,
|
||||||
|
'revision': revision,
|
||||||
|
'updatedAt': updatedAt.toUtc().toIso8601String(),
|
||||||
|
if (lastSyncedAt != null)
|
||||||
|
'lastSyncedAt': lastSyncedAt!.toUtc().toIso8601String(),
|
||||||
|
'dirty': dirty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payload sent to the API (server assigns authoritative revision on conflict).
|
||||||
|
Map<String, dynamic> toApiJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'email': email,
|
||||||
|
'displayName': displayName,
|
||||||
|
'photoUrl': photoUrl,
|
||||||
|
'locale': locale,
|
||||||
|
'timezone': timezone,
|
||||||
|
'onboardingCompleted': onboardingCompleted,
|
||||||
|
'revision': revision,
|
||||||
|
'updatedAt': updatedAt.toUtc().toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
248
lib/repositories/user_profile_repository.dart
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
|
import '../data/local/app_database.dart';
|
||||||
|
import '../data/local/user_profile_local_store.dart';
|
||||||
|
import '../data/remote/profile_api_exception.dart';
|
||||||
|
import '../data/remote/user_profile_remote_store.dart';
|
||||||
|
import '../data/sync/connectivity_service.dart';
|
||||||
|
import '../data/sync/profile_sync_coordinator.dart';
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
import '../models/sync_result.dart';
|
||||||
|
import '../models/user_profile.dart';
|
||||||
|
|
||||||
|
/// Facade for profile reads/writes with Drift (native) and API sync.
|
||||||
|
class UserProfileRepository {
|
||||||
|
UserProfileRepository._();
|
||||||
|
|
||||||
|
static final UserProfileRepository instance = UserProfileRepository._();
|
||||||
|
|
||||||
|
final ConnectivityService _connectivity = ConnectivityService();
|
||||||
|
final UserProfileRemoteStore _remote = UserProfileRemoteStore();
|
||||||
|
|
||||||
|
AppDatabase? _database;
|
||||||
|
UserProfileLocalStore? _local;
|
||||||
|
ProfileSyncCoordinator? _sync;
|
||||||
|
StreamSubscription<bool>? _connectivitySubscription;
|
||||||
|
|
||||||
|
final StreamController<UserProfile?> _profileController =
|
||||||
|
StreamController<UserProfile?>.broadcast();
|
||||||
|
final StreamController<ProfileSyncStatus> _syncStatusController =
|
||||||
|
StreamController<ProfileSyncStatus>.broadcast();
|
||||||
|
|
||||||
|
StreamSubscription<UserProfile?>? _localWatchSubscription;
|
||||||
|
String? _activeUid;
|
||||||
|
UserProfile? _cachedProfile;
|
||||||
|
|
||||||
|
Stream<UserProfile?> get profileStream => _profileController.stream;
|
||||||
|
|
||||||
|
Stream<ProfileSyncStatus> get syncStatusStream =>
|
||||||
|
_syncStatusController.stream;
|
||||||
|
|
||||||
|
UserProfile? get currentProfile => _cachedProfile;
|
||||||
|
|
||||||
|
bool get usesLocalStore => _local != null;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
await _connectivity.initialize();
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
|
_database = AppDatabase();
|
||||||
|
_local = UserProfileLocalStore(_database!);
|
||||||
|
_sync = ProfileSyncCoordinator(
|
||||||
|
local: _local,
|
||||||
|
remote: _remote,
|
||||||
|
connectivity: _connectivity,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_sync = ProfileSyncCoordinator(
|
||||||
|
local: null,
|
||||||
|
remote: _remote,
|
||||||
|
connectivity: _connectivity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectivitySubscription = _connectivity.onlineChanges.listen((
|
||||||
|
bool online,
|
||||||
|
) {
|
||||||
|
if (online && _activeUid != null) {
|
||||||
|
unawaited(sync());
|
||||||
|
} else if (!online) {
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.offline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startSession(AppUser user) async {
|
||||||
|
if (_activeUid == user.uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await endSession();
|
||||||
|
|
||||||
|
_activeUid = user.uid;
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.syncing);
|
||||||
|
|
||||||
|
if (_local != null) {
|
||||||
|
_localWatchSubscription = _local!.watchProfile(user.uid).listen(
|
||||||
|
(UserProfile? profile) {
|
||||||
|
_cachedProfile = profile;
|
||||||
|
_profileController.add(profile);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
UserProfile? profile = await _local!.getProfile(user.uid);
|
||||||
|
profile ??= UserProfile.fromAuth(
|
||||||
|
firebaseUid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
displayName: user.displayName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
);
|
||||||
|
await _local!.saveProfile(profile, dirty: true);
|
||||||
|
_cachedProfile = profile;
|
||||||
|
_profileController.add(profile);
|
||||||
|
} else {
|
||||||
|
await _loadRemoteProfile(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRemoteProfile(AppUser user) async {
|
||||||
|
try {
|
||||||
|
UserProfile? profile = await _remote.fetchProfile();
|
||||||
|
profile ??= UserProfile.fromAuth(
|
||||||
|
firebaseUid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
displayName: user.displayName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
);
|
||||||
|
if (profile.revision == 1 && !profile.dirty) {
|
||||||
|
final UserProfile seeded = profile.copyWith(dirty: true);
|
||||||
|
profile = await _remote.pushProfile(seeded);
|
||||||
|
}
|
||||||
|
_cachedProfile = profile;
|
||||||
|
_profileController.add(profile);
|
||||||
|
} catch (_) {
|
||||||
|
final UserProfile fallback = UserProfile.fromAuth(
|
||||||
|
firebaseUid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
displayName: user.displayName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
);
|
||||||
|
_cachedProfile = fallback;
|
||||||
|
_profileController.add(fallback);
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> endSession() async {
|
||||||
|
await _localWatchSubscription?.cancel();
|
||||||
|
_localWatchSubscription = null;
|
||||||
|
|
||||||
|
if (_activeUid != null && _local != null) {
|
||||||
|
await _local!.clearLocal(_activeUid!);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activeUid = null;
|
||||||
|
_cachedProfile = null;
|
||||||
|
_profileController.add(null);
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProfile(UserProfile profile) async {
|
||||||
|
final UserProfile updated = profile.copyWith(
|
||||||
|
revision: profile.revision + 1,
|
||||||
|
updatedAt: DateTime.now().toUtc(),
|
||||||
|
dirty: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_local != null) {
|
||||||
|
await _local!.saveProfile(updated, dirty: true);
|
||||||
|
} else {
|
||||||
|
_cachedProfile = updated;
|
||||||
|
_profileController.add(updated);
|
||||||
|
await _remote.pushProfile(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(sync());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SyncResult> sync() async {
|
||||||
|
final String? uid = _activeUid;
|
||||||
|
if (uid == null || _sync == null) {
|
||||||
|
return const SyncResult.error('No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kIsWeb && !_connectivity.isOnline) {
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.offline);
|
||||||
|
return const SyncResult.offline();
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.syncing);
|
||||||
|
final SyncResult result = _local == null
|
||||||
|
? await _syncWebOnly()
|
||||||
|
: await _sync!.sync(uid);
|
||||||
|
|
||||||
|
switch (result.kind) {
|
||||||
|
case SyncResultKind.success:
|
||||||
|
case SyncResultKind.conflictResolved:
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.synced);
|
||||||
|
case SyncResultKind.offline:
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.offline);
|
||||||
|
case SyncResultKind.error:
|
||||||
|
_emitSyncStatus(ProfileSyncStatus.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SyncResult> _syncWebOnly() async {
|
||||||
|
final UserProfile? profile = _cachedProfile;
|
||||||
|
if (profile == null) {
|
||||||
|
return const SyncResult.error('No profile loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (profile.dirty) {
|
||||||
|
try {
|
||||||
|
final UserProfile saved = await _remote.pushProfile(profile);
|
||||||
|
_cachedProfile = saved;
|
||||||
|
_profileController.add(saved);
|
||||||
|
} on ProfileConflictException catch (e) {
|
||||||
|
_cachedProfile = e.serverProfile;
|
||||||
|
_profileController.add(e.serverProfile);
|
||||||
|
return const SyncResult.conflictResolved();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final UserProfile? server = await _remote.fetchProfile();
|
||||||
|
if (server != null) {
|
||||||
|
_cachedProfile = server;
|
||||||
|
_profileController.add(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const SyncResult.success();
|
||||||
|
} on ProfileApiException catch (e) {
|
||||||
|
return SyncResult.error(e.toString());
|
||||||
|
} catch (e) {
|
||||||
|
return SyncResult.error(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _connectivitySubscription?.cancel();
|
||||||
|
_connectivity.dispose();
|
||||||
|
await _localWatchSubscription?.cancel();
|
||||||
|
await _profileController.close();
|
||||||
|
await _syncStatusController.close();
|
||||||
|
await _database?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitSyncStatus(ProfileSyncStatus status) {
|
||||||
|
if (!_syncStatusController.isClosed) {
|
||||||
|
_syncStatusController.add(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
199
lib/screens/home_screen.dart
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
import '../models/sync_result.dart';
|
||||||
|
import '../models/user_profile.dart';
|
||||||
|
import '../repositories/user_profile_repository.dart';
|
||||||
|
import '../services/auth_service.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
const HomeScreen({
|
||||||
|
super.key,
|
||||||
|
required this.user,
|
||||||
|
this.profile,
|
||||||
|
this.syncStatus = ProfileSyncStatus.idle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppUser user;
|
||||||
|
final UserProfile? profile;
|
||||||
|
final ProfileSyncStatus syncStatus;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String? photoUrl = profile?.photoUrl ?? user.photoUrl;
|
||||||
|
final String displayName =
|
||||||
|
profile?.displayName ?? user.displayName ?? 'there';
|
||||||
|
final String? email = profile?.email ?? user.email;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
title: const Text('Cyber Hybrid Hub'),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => UserProfileRepository.instance.sync(),
|
||||||
|
tooltip: 'Sync profile',
|
||||||
|
icon: const Icon(Icons.sync),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => AuthService.instance.signOut(),
|
||||||
|
tooltip: 'Sign out',
|
||||||
|
icon: const Icon(Icons.logout),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: DecoratedBox(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[AppColors.background, AppColors.surface],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceElevated,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.accent.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
if (photoUrl != null)
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 36,
|
||||||
|
backgroundImage: NetworkImage(photoUrl),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const CircleAvatar(
|
||||||
|
radius: 36,
|
||||||
|
child: Icon(Icons.person, size: 36),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Welcome, $displayName',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (email != null) ...<Widget>[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
email,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_SyncStatusChip(status: syncStatus),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: AppColors.success,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'You\'re signed in',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.success,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
profile?.dirty == true
|
||||||
|
? 'Profile saved locally. Will sync when online.'
|
||||||
|
: 'Profile synced with your account.',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (UserProfileRepository.instance.usesLocalStore) ...<Widget>[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: profile == null
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final UserProfile current = profile!;
|
||||||
|
await UserProfileRepository.instance.updateProfile(
|
||||||
|
current.copyWith(
|
||||||
|
onboardingCompleted:
|
||||||
|
!current.onboardingCompleted,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.toggle_on_outlined),
|
||||||
|
label: Text(
|
||||||
|
profile?.onboardingCompleted == true
|
||||||
|
? 'Mark onboarding incomplete'
|
||||||
|
: 'Mark onboarding complete',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SyncStatusChip extends StatelessWidget {
|
||||||
|
const _SyncStatusChip({required this.status});
|
||||||
|
|
||||||
|
final ProfileSyncStatus status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ({String label, Color color, IconData icon}) style = switch (status) {
|
||||||
|
ProfileSyncStatus.syncing => (
|
||||||
|
label: 'Syncing…',
|
||||||
|
color: AppColors.accent,
|
||||||
|
icon: Icons.sync,
|
||||||
|
),
|
||||||
|
ProfileSyncStatus.synced => (
|
||||||
|
label: 'Synced',
|
||||||
|
color: AppColors.success,
|
||||||
|
icon: Icons.cloud_done_outlined,
|
||||||
|
),
|
||||||
|
ProfileSyncStatus.offline => (
|
||||||
|
label: 'Offline',
|
||||||
|
color: Colors.orange,
|
||||||
|
icon: Icons.cloud_off_outlined,
|
||||||
|
),
|
||||||
|
ProfileSyncStatus.error => (
|
||||||
|
label: 'Sync error',
|
||||||
|
color: Colors.redAccent,
|
||||||
|
icon: Icons.error_outline,
|
||||||
|
),
|
||||||
|
ProfileSyncStatus.idle => (
|
||||||
|
label: 'Ready',
|
||||||
|
color: AppColors.accent,
|
||||||
|
icon: Icons.cloud_queue_outlined,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Chip(
|
||||||
|
avatar: Icon(style.icon, size: 18, color: style.color),
|
||||||
|
label: Text(style.label),
|
||||||
|
side: BorderSide(color: style.color.withValues(alpha: 0.4)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
464
lib/screens/landing_screen.dart
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../widgets/benefit_row.dart';
|
||||||
|
import '../widgets/google_sign_in_button.dart';
|
||||||
|
|
||||||
|
class LandingScreen extends StatefulWidget {
|
||||||
|
const LandingScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LandingScreen> createState() => _LandingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LandingScreenState extends State<LandingScreen> {
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: DecoratedBox(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: <Color>[
|
||||||
|
AppColors.background,
|
||||||
|
Color(0xFF0C1222),
|
||||||
|
Color(0xFF0A1628),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
const _BackgroundGlow(),
|
||||||
|
SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
final bool wide = constraints.maxWidth >= 720;
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 960),
|
||||||
|
child: wide
|
||||||
|
? _WideLayout(
|
||||||
|
errorMessage: _errorMessage,
|
||||||
|
onError: _handleError,
|
||||||
|
)
|
||||||
|
: _NarrowLayout(
|
||||||
|
errorMessage: _errorMessage,
|
||||||
|
onError: _handleError,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleError(Object error) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = _friendlyError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _friendlyError(Object error) {
|
||||||
|
if (error is FirebaseAuthException) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'unauthorized-domain':
|
||||||
|
final String origin = kIsWeb ? Uri.base.origin : 'this app';
|
||||||
|
return 'Add $origin under Firebase Console → Authentication → '
|
||||||
|
'Settings → Authorized domains.';
|
||||||
|
case 'operation-not-allowed':
|
||||||
|
return 'Google sign-in is not enabled. Turn on the Google provider '
|
||||||
|
'in Firebase Console → Authentication → Sign-in method.';
|
||||||
|
case 'popup-blocked':
|
||||||
|
return 'The sign-in popup was blocked. Allow popups for this site '
|
||||||
|
'or try again.';
|
||||||
|
case 'popup-closed-by-user':
|
||||||
|
case 'cancelled-popup-request':
|
||||||
|
return 'Sign-in was cancelled. Tap below to try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String raw = error.toString();
|
||||||
|
if (raw.contains('missing initial state') ||
|
||||||
|
raw.contains('sessionStorage')) {
|
||||||
|
return 'This browser blocked sign-in storage (common in Firefox). '
|
||||||
|
'Set googleWebOAuthClientId in lib/config/auth_config.dart, allow '
|
||||||
|
'popups for this site, or try Chrome.';
|
||||||
|
}
|
||||||
|
if (raw.contains('canceled') || raw.contains('cancelled')) {
|
||||||
|
return 'Sign-in was cancelled. Tap below to try again.';
|
||||||
|
}
|
||||||
|
if (raw.contains('network')) {
|
||||||
|
return 'Network error. Check your connection and try again.';
|
||||||
|
}
|
||||||
|
if (raw.contains('googleWebOAuthClientId')) {
|
||||||
|
return raw.replaceFirst('Bad state: ', '');
|
||||||
|
}
|
||||||
|
return 'Could not sign in. Please try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackgroundGlow extends StatelessWidget {
|
||||||
|
const _BackgroundGlow();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned(
|
||||||
|
top: -120,
|
||||||
|
right: -80,
|
||||||
|
child: Container(
|
||||||
|
width: 320,
|
||||||
|
height: 320,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: RadialGradient(
|
||||||
|
colors: <Color>[
|
||||||
|
AppColors.accent.withValues(alpha: 0.18),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -100,
|
||||||
|
left: -60,
|
||||||
|
child: Container(
|
||||||
|
width: 280,
|
||||||
|
height: 280,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: RadialGradient(
|
||||||
|
colors: <Color>[
|
||||||
|
AppColors.accentMuted.withValues(alpha: 0.12),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NarrowLayout extends StatelessWidget {
|
||||||
|
const _NarrowLayout({required this.errorMessage, required this.onError});
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
final void Function(Object error) onError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
const _BrandHeader(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
const _HeroCopy(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
const _BenefitsList(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_CtaSection(errorMessage: errorMessage, onError: onError),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const _TrustFooter(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WideLayout extends StatelessWidget {
|
||||||
|
const _WideLayout({required this.errorMessage, required this.onError});
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
final void Function(Object error) onError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_BrandHeader(),
|
||||||
|
SizedBox(height: 40),
|
||||||
|
_HeroCopy(),
|
||||||
|
SizedBox(height: 36),
|
||||||
|
_BenefitsList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 48),
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: _CtaCard(errorMessage: errorMessage, onError: onError),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BrandHeader extends StatelessWidget {
|
||||||
|
const _BrandHeader();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: <Color>[AppColors.accent, AppColors.accentMuted],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
color: AppColors.background,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Cyber Hybrid Hub',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeroCopy extends StatelessWidget {
|
||||||
|
const _HeroCopy();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.success.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.success.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(Icons.bolt, size: 16, color: AppColors.success),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Free to get started',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.success,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Your hybrid security\ncommand center',
|
||||||
|
style: Theme.of(context).textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Unify threat monitoring, team collaboration, and compliance '
|
||||||
|
'workflows in one secure workspace — built for modern hybrid teams.',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
fontSize: 17,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BenefitsList extends StatelessWidget {
|
||||||
|
const _BenefitsList();
|
||||||
|
|
||||||
|
static const List<({IconData icon, String title, String subtitle})> _items =
|
||||||
|
<({IconData icon, String title, String subtitle})>[
|
||||||
|
(
|
||||||
|
icon: Icons.radar,
|
||||||
|
title: 'Real-time threat visibility',
|
||||||
|
subtitle: 'See alerts and incidents as they happen, not hours later.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
title: 'Built for hybrid teams',
|
||||||
|
subtitle: 'On-site and remote staff share one source of truth.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
icon: Icons.verified_user_outlined,
|
||||||
|
title: 'Sign in securely with Google',
|
||||||
|
subtitle: 'No new passwords — enterprise-ready OAuth in one tap.',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int i = 0; i < _items.length; i++) ...<Widget>[
|
||||||
|
if (i > 0) const SizedBox(height: 20),
|
||||||
|
BenefitRow(
|
||||||
|
icon: _items[i].icon,
|
||||||
|
title: _items[i].title,
|
||||||
|
subtitle: _items[i].subtitle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CtaSection extends StatelessWidget {
|
||||||
|
const _CtaSection({required this.errorMessage, required this.onError});
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
final void Function(Object error) onError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _CtaCard(errorMessage: errorMessage, onError: onError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CtaCard extends StatelessWidget {
|
||||||
|
const _CtaCard({required this.errorMessage, required this.onError});
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
final void Function(Object error) onError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceElevated,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.accent.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 40,
|
||||||
|
offset: const Offset(0, 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'Get started in seconds',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
fontSize: 22,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Use your Google account — no credit card required.',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (errorMessage != null) ...<Widget>[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.error.withValues(
|
||||||
|
alpha: 0.12,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
errorMessage!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
GoogleSignInButton(
|
||||||
|
label: 'Get started with Google',
|
||||||
|
onError: onError,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const _SocialProof(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SocialProof extends StatelessWidget {
|
||||||
|
const _SocialProof();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(Icons.lock_outline, size: 14, color: AppColors.textSecondary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Secured by Firebase · Google OAuth',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrustFooter extends StatelessWidget {
|
||||||
|
const _TrustFooter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
'By continuing, you agree to our Terms of Service and Privacy Policy.',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/services/auth_service.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||||
|
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
import 'auth_service_firebase.dart';
|
||||||
|
import 'auth_service_linux.dart';
|
||||||
|
|
||||||
|
/// Unified auth API for mobile, web, and Linux desktop development.
|
||||||
|
class AuthService {
|
||||||
|
AuthService._();
|
||||||
|
|
||||||
|
static final AuthService instance = AuthService._();
|
||||||
|
|
||||||
|
/// True when running the Linux desktop app (not unit tests on a Linux host).
|
||||||
|
static bool get isLinuxDesktop {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const bool.fromEnvironment('FLUTTER_TEST')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return defaultTargetPlatform == TargetPlatform.linux;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<AppUser?> get authStateChanges => isLinuxDesktop
|
||||||
|
? AuthServiceLinux.instance.authStateChanges
|
||||||
|
: AuthServiceFirebase.instance.authStateChanges;
|
||||||
|
|
||||||
|
AppUser? get currentUser => isLinuxDesktop
|
||||||
|
? AuthServiceLinux.instance.currentUser
|
||||||
|
: AuthServiceFirebase.instance.currentUser;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (isLinuxDesktop) {
|
||||||
|
await AuthServiceLinux.instance.initialize();
|
||||||
|
} else {
|
||||||
|
await AuthServiceFirebase.instance.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signInWithGoogle() async {
|
||||||
|
if (isLinuxDesktop) {
|
||||||
|
await AuthServiceLinux.instance.signInWithGoogle();
|
||||||
|
} else {
|
||||||
|
await AuthServiceFirebase.instance.signInWithGoogle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
if (isLinuxDesktop) {
|
||||||
|
await AuthServiceLinux.instance.signOut();
|
||||||
|
} else {
|
||||||
|
await AuthServiceFirebase.instance.signOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Firebase ID token for API requests. Returns null when signed out.
|
||||||
|
Future<String?> getIdToken({bool forceRefresh = false}) async {
|
||||||
|
if (isLinuxDesktop) {
|
||||||
|
return AuthServiceLinux.instance.getIdToken(
|
||||||
|
forceRefresh: forceRefresh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AuthServiceFirebase.instance.getIdToken(
|
||||||
|
forceRefresh: forceRefresh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
lib/services/auth_service_firebase.dart
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
|
|
||||||
|
import '../config/auth_config.dart';
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
|
||||||
|
class AuthServiceFirebase {
|
||||||
|
AuthServiceFirebase._();
|
||||||
|
|
||||||
|
static final AuthServiceFirebase instance = AuthServiceFirebase._();
|
||||||
|
|
||||||
|
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
|
||||||
|
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
|
||||||
|
|
||||||
|
bool _googleSignInReady = false;
|
||||||
|
|
||||||
|
Stream<AppUser?> get authStateChanges =>
|
||||||
|
_firebaseAuth.authStateChanges().map(_mapUser);
|
||||||
|
|
||||||
|
AppUser? get currentUser => _mapUser(_firebaseAuth.currentUser);
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
if (googleWebOAuthClientId != null) {
|
||||||
|
await _googleSignIn.initialize(clientId: googleWebOAuthClientId);
|
||||||
|
_googleSignInReady = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (googleWebOAuthClientId != null) {
|
||||||
|
await _googleSignIn.initialize(
|
||||||
|
serverClientId: googleWebOAuthClientId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _googleSignIn.initialize();
|
||||||
|
}
|
||||||
|
_googleSignInReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signInWithGoogle() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
await _signInWithGoogleWeb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureGoogleSignInReady();
|
||||||
|
|
||||||
|
final GoogleSignInAccount account = await _googleSignIn.authenticate();
|
||||||
|
final String? idToken = account.authentication.idToken;
|
||||||
|
if (idToken == null) {
|
||||||
|
throw StateError('Google Sign-In did not return an ID token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final credential = GoogleAuthProvider.credential(idToken: idToken);
|
||||||
|
await _firebaseAuth.signInWithCredential(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web: popup avoids Firebase redirect sessionStorage (broken under ETP).
|
||||||
|
/// Falls back to Google Identity Services + [signInWithCredential] when set up.
|
||||||
|
Future<void> _signInWithGoogleWeb() async {
|
||||||
|
try {
|
||||||
|
await _firebaseAuth.signInWithPopup(GoogleAuthProvider());
|
||||||
|
return;
|
||||||
|
} on FirebaseAuthException catch (e) {
|
||||||
|
if (_isUserCancelledAuth(e)) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_googleSignInReady) {
|
||||||
|
throw StateError(
|
||||||
|
'Google sign-in failed in this browser (often Firefox with strict '
|
||||||
|
'privacy). Set googleWebOAuthClientId in lib/config/auth_config.dart '
|
||||||
|
'(Firebase Console → Authentication → Google → Web SDK configuration) '
|
||||||
|
'and add ${Uri.base.origin} to that client\'s authorized JavaScript '
|
||||||
|
'origins in Google Cloud Console.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final GoogleSignInAccount account = await _googleSignIn.authenticate();
|
||||||
|
final String? idToken = account.authentication.idToken;
|
||||||
|
if (idToken == null) {
|
||||||
|
throw StateError('Google Sign-In did not return an ID token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _firebaseAuth.signInWithCredential(
|
||||||
|
GoogleAuthProvider.credential(idToken: idToken),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
if (_googleSignInReady) {
|
||||||
|
await _googleSignIn.signOut();
|
||||||
|
}
|
||||||
|
await _firebaseAuth.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getIdToken({bool forceRefresh = false}) async {
|
||||||
|
final User? user = _firebaseAuth.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user.getIdToken(forceRefresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureGoogleSignInReady() {
|
||||||
|
if (_googleSignInReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw StateError(
|
||||||
|
'Google Sign-In is not configured. Set googleWebOAuthClientId in '
|
||||||
|
'lib/config/auth_config.dart for Android, or run a full restart after '
|
||||||
|
'changing auth settings.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isUserCancelledAuth(FirebaseAuthException e) {
|
||||||
|
return e.code == 'popup-closed-by-user' ||
|
||||||
|
e.code == 'cancelled-popup-request';
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser? _mapUser(User? user) {
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return AppUser(
|
||||||
|
uid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
displayName: user.displayName,
|
||||||
|
photoUrl: user.photoURL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
lib/services/auth_service_linux.dart
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:google_sign_in_all_platforms/google_sign_in_all_platforms.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../config/auth_config.dart';
|
||||||
|
import '../firebase_options.dart';
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
|
||||||
|
class AuthServiceLinux {
|
||||||
|
AuthServiceLinux._();
|
||||||
|
|
||||||
|
static final AuthServiceLinux instance = AuthServiceLinux._();
|
||||||
|
|
||||||
|
final StreamController<AppUser?> _authController =
|
||||||
|
StreamController<AppUser?>.broadcast();
|
||||||
|
|
||||||
|
GoogleSignIn? _googleSignIn;
|
||||||
|
AppUser? _currentUser;
|
||||||
|
String? _idToken;
|
||||||
|
String? _refreshToken;
|
||||||
|
DateTime? _tokenExpiry;
|
||||||
|
|
||||||
|
Stream<AppUser?> get authStateChanges => _authController.stream;
|
||||||
|
|
||||||
|
AppUser? get currentUser => _currentUser;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (!_hasOAuthConfig) {
|
||||||
|
_emitUser(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_googleSignIn = GoogleSignIn(
|
||||||
|
params: GoogleSignInParams(
|
||||||
|
clientId: googleWebOAuthClientId!,
|
||||||
|
clientSecret: googleOAuthDesktopClientSecret!,
|
||||||
|
scopes: const <String>['openid', 'profile', 'email'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_googleSignIn!.authenticationState.listen((GoogleSignInCredentials? creds) {
|
||||||
|
if (creds == null) {
|
||||||
|
_emitUser(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final GoogleSignInCredentials? restored =
|
||||||
|
await _googleSignIn!.silentSignIn();
|
||||||
|
if (restored?.idToken != null) {
|
||||||
|
try {
|
||||||
|
final AppUser user = await _signInToFirebaseWithIdp(restored!.idToken!);
|
||||||
|
_emitUser(user);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore stale desktop sessions on startup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _hasOAuthConfig =>
|
||||||
|
googleWebOAuthClientId != null &&
|
||||||
|
googleOAuthDesktopClientSecret != null;
|
||||||
|
|
||||||
|
Future<void> signInWithGoogle() async {
|
||||||
|
_validateOAuthConfig();
|
||||||
|
final GoogleSignInCredentials? credentials = await _googleSignIn!.signIn();
|
||||||
|
if (credentials == null) {
|
||||||
|
throw StateError('Google Sign-In was cancelled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? idToken = credentials.idToken;
|
||||||
|
if (idToken == null) {
|
||||||
|
throw StateError('Google Sign-In did not return an ID token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final AppUser user = await _signInToFirebaseWithIdp(idToken);
|
||||||
|
_emitUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
await _googleSignIn?.signOut();
|
||||||
|
_clearTokens();
|
||||||
|
_emitUser(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getIdToken({bool forceRefresh = false}) async {
|
||||||
|
if (_currentUser == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!forceRefresh &&
|
||||||
|
_idToken != null &&
|
||||||
|
_tokenExpiry != null &&
|
||||||
|
DateTime.now().isBefore(_tokenExpiry!)) {
|
||||||
|
return _idToken;
|
||||||
|
}
|
||||||
|
if (_refreshToken != null) {
|
||||||
|
await _refreshIdToken();
|
||||||
|
return _idToken;
|
||||||
|
}
|
||||||
|
return _idToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitUser(AppUser? user) {
|
||||||
|
_currentUser = user;
|
||||||
|
if (user == null) {
|
||||||
|
_clearTokens();
|
||||||
|
}
|
||||||
|
_authController.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearTokens() {
|
||||||
|
_idToken = null;
|
||||||
|
_refreshToken = null;
|
||||||
|
_tokenExpiry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _storeTokens(Map<String, dynamic> data) {
|
||||||
|
_idToken = data['idToken'] as String?;
|
||||||
|
_refreshToken = data['refreshToken'] as String?;
|
||||||
|
final int expiresIn = (data['expiresIn'] as String?) != null
|
||||||
|
? int.parse(data['expiresIn'] as String)
|
||||||
|
: (data['expiresIn'] as num?)?.toInt() ?? 3600;
|
||||||
|
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshIdToken() async {
|
||||||
|
if (_refreshToken == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Uri uri = Uri.parse(
|
||||||
|
'https://securetoken.googleapis.com/v1/token'
|
||||||
|
'?key=${DefaultFirebaseOptions.web.apiKey}',
|
||||||
|
);
|
||||||
|
final http.Response response = await http.post(
|
||||||
|
uri,
|
||||||
|
headers: const <String, String>{
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: 'grant_type=refresh_token&refresh_token=$_refreshToken',
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
_clearTokens();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Map<String, dynamic> data =
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
_idToken = data['id_token'] as String?;
|
||||||
|
_refreshToken = data['refresh_token'] as String? ?? _refreshToken;
|
||||||
|
final int expiresIn = (data['expires_in'] as num?)?.toInt() ?? 3600;
|
||||||
|
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _validateOAuthConfig() {
|
||||||
|
if (!_hasOAuthConfig) {
|
||||||
|
throw StateError(
|
||||||
|
'Linux desktop auth requires googleWebOAuthClientId and '
|
||||||
|
'googleOAuthDesktopClientSecret in lib/config/auth_config.dart. '
|
||||||
|
'Create a Web application OAuth client in Google Cloud Console '
|
||||||
|
'(same project as Firebase) and add '
|
||||||
|
'http://127.0.0.1 as an authorized redirect URI.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AppUser> _signInToFirebaseWithIdp(String idToken) async {
|
||||||
|
final Uri uri = Uri.parse(
|
||||||
|
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
|
||||||
|
'?key=${DefaultFirebaseOptions.web.apiKey}',
|
||||||
|
);
|
||||||
|
|
||||||
|
final http.Response response = await http.post(
|
||||||
|
uri,
|
||||||
|
headers: const <String, String>{'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(<String, dynamic>{
|
||||||
|
'postBody': 'id_token=$idToken&providerId=google.com',
|
||||||
|
'requestUri': 'http://localhost',
|
||||||
|
'returnIdpCredential': true,
|
||||||
|
'returnSecureToken': true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw StateError(
|
||||||
|
'Firebase sign-in failed (${response.statusCode}): ${response.body}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> data =
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
_storeTokens(data);
|
||||||
|
|
||||||
|
return AppUser(
|
||||||
|
uid: data['localId'] as String,
|
||||||
|
email: data['email'] as String?,
|
||||||
|
displayName: data['displayName'] as String?,
|
||||||
|
photoUrl: data['photoUrl'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/theme/app_theme.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
abstract final class AppColors {
|
||||||
|
static const Color background = Color(0xFF070B14);
|
||||||
|
static const Color surface = Color(0xFF111827);
|
||||||
|
static const Color surfaceElevated = Color(0xFF1A2332);
|
||||||
|
static const Color accent = Color(0xFF22D3EE);
|
||||||
|
static const Color accentMuted = Color(0xFF0891B2);
|
||||||
|
static const Color textPrimary = Color(0xFFF8FAFC);
|
||||||
|
static const Color textSecondary = Color(0xFF94A3B8);
|
||||||
|
static const Color success = Color(0xFF34D399);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData buildAppTheme() {
|
||||||
|
const ColorScheme scheme = ColorScheme.dark(
|
||||||
|
primary: AppColors.accent,
|
||||||
|
onPrimary: AppColors.background,
|
||||||
|
secondary: AppColors.accentMuted,
|
||||||
|
surface: AppColors.surface,
|
||||||
|
onSurface: AppColors.textPrimary,
|
||||||
|
error: Color(0xFFF87171),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: scheme,
|
||||||
|
scaffoldBackgroundColor: AppColors.background,
|
||||||
|
textTheme: const TextTheme(
|
||||||
|
headlineLarge: TextStyle(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.15,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
headlineMedium: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.5,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
labelLarge: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.background,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
51
lib/widgets/benefit_row.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
|
||||||
|
class BenefitRow extends StatelessWidget {
|
||||||
|
const BenefitRow({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.accent.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.accent.withValues(alpha: 0.25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: AppColors.accent, size: 22),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(subtitle, style: Theme.of(context).textTheme.bodyLarge),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/widgets/google_sign_in_button.dart
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../services/auth_service.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
|
||||||
|
class GoogleSignInButton extends StatefulWidget {
|
||||||
|
const GoogleSignInButton({
|
||||||
|
super.key,
|
||||||
|
this.label = 'Continue with Google',
|
||||||
|
this.compact = false,
|
||||||
|
this.onSignedIn,
|
||||||
|
this.onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final bool compact;
|
||||||
|
final VoidCallback? onSignedIn;
|
||||||
|
final void Function(Object error)? onError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GoogleSignInButton> createState() => _GoogleSignInButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GoogleSignInButtonState extends State<GoogleSignInButton> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
Future<void> _signIn() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
await AuthService.instance.signInWithGoogle();
|
||||||
|
widget.onSignedIn?.call();
|
||||||
|
} catch (error) {
|
||||||
|
widget.onError?.call(error);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double verticalPadding = widget.compact ? 14 : 16;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _isLoading ? null : _signIn,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: const Color(0xFF1F2937),
|
||||||
|
disabledBackgroundColor: Colors.white.withValues(alpha: 0.7),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: verticalPadding),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: AppColors.background,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const _GoogleLogo(size: 22),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
widget.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1F2937),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GoogleLogo extends StatelessWidget {
|
||||||
|
const _GoogleLogo({required this.size});
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: CustomPaint(painter: _GoogleLogoPainter()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GoogleLogoPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final double w = size.width;
|
||||||
|
final double h = size.height;
|
||||||
|
final Offset center = Offset(w / 2, h / 2);
|
||||||
|
final double r = w * 0.42;
|
||||||
|
|
||||||
|
void arc(Color color, double start, double sweep) {
|
||||||
|
final Paint paint = Paint()
|
||||||
|
..color = color
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = w * 0.18
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: r),
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
false,
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
arc(const Color(0xFF4285F4), -0.4, 1.6);
|
||||||
|
arc(const Color(0xFFEA4335), 1.2, 1.3);
|
||||||
|
arc(const Color(0xFFFBBC05), 2.5, 1.3);
|
||||||
|
arc(const Color(0xFF34A853), 3.8, 1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
79
lib/widgets/profile_session.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models/app_user.dart';
|
||||||
|
import '../models/sync_result.dart';
|
||||||
|
import '../models/user_profile.dart';
|
||||||
|
import '../repositories/user_profile_repository.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
|
||||||
|
/// Starts a profile sync session for [user] and exposes profile + sync state.
|
||||||
|
class ProfileSession extends StatefulWidget {
|
||||||
|
const ProfileSession({
|
||||||
|
super.key,
|
||||||
|
required this.user,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppUser user;
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
UserProfile? profile,
|
||||||
|
ProfileSyncStatus syncStatus,
|
||||||
|
) builder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileSession> createState() => _ProfileSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileSessionState extends State<ProfileSession> {
|
||||||
|
late final Future<void> _sessionReady;
|
||||||
|
ProfileSyncStatus _syncStatus = ProfileSyncStatus.idle;
|
||||||
|
StreamSubscription<ProfileSyncStatus>? _syncStatusSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_sessionReady = UserProfileRepository.instance.startSession(widget.user);
|
||||||
|
_syncStatusSubscription =
|
||||||
|
UserProfileRepository.instance.syncStatusStream.listen((
|
||||||
|
ProfileSyncStatus status,
|
||||||
|
) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _syncStatus = status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_syncStatusSubscription?.cancel();
|
||||||
|
UserProfileRepository.instance.endSession();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<void>(
|
||||||
|
future: _sessionReady,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<void> sessionSnap) {
|
||||||
|
if (sessionSnap.connectionState != ConnectionState.done) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColors.accent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamBuilder<UserProfile?>(
|
||||||
|
stream: UserProfileRepository.instance.profileStream,
|
||||||
|
initialData: UserProfileRepository.instance.currentProfile,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<UserProfile?> snap) {
|
||||||
|
return widget.builder(context, snap.data, _syncStatus);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
linux/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||