Compare commits

...

42 Commits

Author SHA1 Message Date
Hamza-Ayed
f3daf4f3d8 chore: comment out SceneDelegate implementation and add devtools_options configuration file 2026-06-20 17:01:05 +03:00
Hamza-Ayed
9f43eaf8ef Sync update: 2026-06-20 16:53:51 2026-06-20 16:53:51 +03:00
Hamza-Ayed
10d4651965 Sync update: 2026-05-22 19:31:19 2026-05-22 19:31:19 +03:00
Hamza-Ayed
22f1bba6ac Sync update: 2026-05-19 23:27:14 2026-05-19 23:27:14 +03:00
Hamza-Ayed
1eec712c58 Sync update: 2026-05-18 21:13:35 2026-05-18 21:13:35 +03:00
Hamza-Ayed
340a22fffa docs: add comprehensive server architecture and api documentation 2026-05-18 21:13:13 +03:00
Hamza-Ayed
a7d7eaec9d feat: add full-screen image viewer and reactive ack icons 2026-05-18 20:55:12 +03:00
Hamza-Ayed
14d30f19bf fix: restore send_message and send_media WebSocket packet handlers in server.js 2026-05-18 20:34:36 +03:00
Hamza-Ayed
224bed32b5 fix: automate migration of session-whatsapp-bridge to session-slot-1 on startup 2026-05-18 20:26:06 +03:00
Hamza-Ayed
7a1997c329 fix: convert QR to base64 DataURL and migrate old session to Slot 1 dynamically 2026-05-18 20:21:17 +03:00
Hamza-Ayed
0ec9b2e3b2 feat: complete migration to 6-slot multi-tenant registry with MySQL message archiving 2026-05-18 20:15:53 +03:00
Hamza-Ayed
0498575e51 feat: ignore background and local notifications for muted chats 2026-05-18 19:53:59 +03:00
Hamza-Ayed
1d20d40fd8 feat: add /api/avatar endpoint and real-time poll vote event WS broadaster 2026-05-18 19:47:42 +03:00
Hamza-Ayed
39b028a85c feat: implement native interactive WhatsApp Polls REST API endpoint (/api/send-poll) 2026-05-18 19:44:09 +03:00
Hamza-Ayed
9490a2d628 fix: sanitize base64 strings in REST API by stripping data url prefixes and whitespaces 2026-05-18 19:42:06 +03:00
Hamza-Ayed
5717d7047e feat: expose REST API endpoints (/api/send, /api/send-media) for universal messaging proxy usage 2026-05-18 19:32:44 +03:00
Hamza-Ayed
123902a6b1 fix: resolve duplicate background notifications and format contact names in local notification titles 2026-05-18 18:53:24 +03:00
Hamza-Ayed
b3ef0b89f6 fix: resolve duplicate path and fs declarations and implement smart .env multi-path resolution 2026-05-18 18:32:31 +03:00
Hamza-Ayed
6882d6e952 security: support secure env-based firebase admin configurations and ignore sensitive keys in git 2026-05-18 18:27:08 +03:00
Hamza-Ayed
79ba52cb7d feat: implement Firebase Admin SDK push notification logic in server.js with fallback support 2026-05-18 18:22:27 +03:00
Hamza-Ayed
92d59b0f30 feat: trigger local notification directly from incoming WebSocket new_message event 2026-05-18 18:19:03 +03:00
Hamza-Ayed
cfc1fd0a8e fix: resolve iOS APNS token waiting loop in FirebaseService to guarantee successful FCM registration 2026-05-18 18:07:36 +03:00
Hamza-Ayed
60139d98c5 Sync update: 2026-05-18 17:57:04 2026-05-18 17:57:04 +03:00
Hamza-Ayed
cb4b423304 feat: enable macOS sandbox internet client network access and configure flutter_launcher_icons with premium generated app icon 2026-05-18 17:51:37 +03:00
Hamza-Ayed
a64725397e fix: downgrade record_platform_interface override to 1.2.0 to bypass startStream in record_linux 2026-05-18 17:39:28 +03:00
Hamza-Ayed
065855d596 fix: override record_platform_interface to 1.4.0 in pubspec.yaml to fix RecordLinux compilation mismatch 2026-05-18 17:37:04 +03:00
Hamza-Ayed
c1b149cc21 feat: implement real cross-platform voice recording utilizing record package with mic permission configuration 2026-05-18 17:32:31 +03:00
Hamza-Ayed
e18f4195b9 fix: resolve DarwinAudioError on iOS using temporary .mp3 extension and upgrade mock Voice Note base64 to valid silent MP3 2026-05-18 17:29:24 +03:00
Hamza-Ayed
e28d985c10 feat: convert outgoing audio to MP3 inside send_media case to bypass getAudioDuration 2026-05-18 17:22:04 +03:00
Hamza-Ayed
4ccd90dad3 feat: integrate real AudioPlayer, real ImagePicker for Camera/Gallery, and on-the-fly OGG-to-MP3 converter on server 2026-05-18 17:14:43 +03:00
Hamza-Ayed
25bdf1fba1 feat: complete interactive audio player, contact resolver, unread clearance, and media sending 2026-05-18 16:51:29 +03:00
Hamza-Ayed
56f29b8306 fix: stabilize iOS launch and remove SceneDelegate dependency 2026-05-18 16:41:16 +03:00
Hamza-Ayed
7c1fbd6696 fix: add retry & delay to downloadMedia in server.js 2026-05-18 16:41:12 +03:00
Hamza-Ayed
f2a14ce23e Sync update: 2026-05-18 16:24:26 2026-05-18 16:24:26 +03:00
Hamza-Ayed
905819a1d5 Sync update: 2026-05-18 16:14:25 2026-05-18 16:14:25 +03:00
Hamza-Ayed
7b6e4b3111 Sync update: 2026-05-18 16:06:07 2026-05-18 16:06:07 +03:00
Hamza-Ayed
215ff9cc1c Sync update: 2026-05-18 16:01:39 2026-05-18 16:01:39 +03:00
Hamza-Ayed
48c897b709 Sync update: 2026-05-18 15:57:09 2026-05-18 15:57:09 +03:00
Hamza-Ayed
b9b72247b0 Sync update: 2026-05-18 15:53:13 2026-05-18 15:53:14 +03:00
Hamza-Ayed
4c68fcc2b9 Sync update: 2026-05-18 15:46:42 2026-05-18 15:46:42 +03:00
Hamza-Ayed
82650b8c1f Sync update: 2026-05-18 15:45:06 2026-05-18 15:45:07 +03:00
Hamza-Ayed
30d32df1c0 Sync update: 2026-05-18 14:22:09 2026-05-18 14:22:09 +03:00
170 changed files with 9055 additions and 852 deletions

6
.gitignore vendored
View File

@@ -24,3 +24,9 @@ whatsapp_app/android/local.properties
.idea/
.vscode/
*.swp
# Sensitive Configurations
whatsapp_bridge/serviceAccountKey.json
whatsapp_bridge/fcm_token.json
whatsapp_bridge/.env
whatsapp_bridge/.env.*

45
whatsapp_app/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
whatsapp_app/.metadata Normal file
View 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: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: android
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: ios
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: linux
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: macos
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: web
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: windows
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
# 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'

17
whatsapp_app/README.md Normal file
View File

@@ -0,0 +1,17 @@
# mywhatsapp
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.

View 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
whatsapp_app/android/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,47 @@
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.intaleqapp.mywhatsapp"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.intaleqapp.mywhatsapp"
// 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")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "146827764316",
"project_id": "mywhatsapp-inta",
"storage_bucket": "mywhatsapp-inta.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:146827764316:android:75425f8baccb6891843c99",
"android_client_info": {
"package_name": "com.intaleqapp.mywhatsapp"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBqCFyXUfd1VjX3htG0Z_-gH4hFOkG31C0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View 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>

View File

@@ -0,0 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="mywhatsapp"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<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>

View File

@@ -0,0 +1,5 @@
package com.intaleqapp.mywhatsapp
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View 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>

View 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>

View 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>

View 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)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View 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-8.14-all.zip

View File

@@ -0,0 +1,29 @@
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 "8.11.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"mywhatsapp-inta","appId":"1:146827764316:android:75425f8baccb6891843c99","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"mywhatsapp-inta","appId":"1:146827764316:ios:c240ccd825bdb66d843c99","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"mywhatsapp-inta","appId":"1:146827764316:ios:c240ccd825bdb66d843c99","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"mywhatsapp-inta","configurations":{"android":"1:146827764316:android:75425f8baccb6891843c99","ios":"1:146827764316:ios:c240ccd825bdb66d843c99","macos":"1:146827764316:ios:c240ccd825bdb66d843c99","web":"1:146827764316:web:99b4d29684ee9a26843c99","windows":"1:146827764316:web:8812ccd133ed647e843c99"}}}}}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
flutter_launcher_icons:
android: "launcher_icon"
ios: true
macos:
generate: true
image_path: "/Users/hamzaaleghwairyeen/.gemini/antigravity/brain/0e47babb-1724-4ba2-b8fd-b88689535c32/app_icon_1779115871023.png"
image_path: "/Users/hamzaaleghwairyeen/.gemini/antigravity/brain/0e47babb-1724-4ba2-b8fd-b88689535c32/app_icon_1779115871023.png"

34
whatsapp_app/ios/.gitignore vendored Normal file
View 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

View 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>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

48
whatsapp_app/ios/Podfile Normal file
View File

@@ -0,0 +1,48 @@
# Uncomment this line to define a global platform for your project
platform :ios, '17.0'
use_modular_headers!
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
# use_frameworks! :linkage => :static
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
end
end
end

View File

@@ -0,0 +1,164 @@
PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- Firebase/CoreOnly (10.25.0):
- FirebaseCore (= 10.25.0)
- Firebase/Messaging (10.25.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 10.25.0)
- firebase_core (2.32.0):
- Firebase/CoreOnly (= 10.25.0)
- Flutter
- firebase_messaging (14.9.4):
- Firebase/Messaging (= 10.25.0)
- firebase_core
- Flutter
- FirebaseCore (10.25.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.12)
- GoogleUtilities/Logger (~> 7.12)
- FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseInstallations (10.29.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseMessaging (10.25.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3)
- GoogleUtilities/AppDelegateSwizzler (~> 7.8)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- Flutter (1.0.0)
- flutter_contacts (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/AppDelegateSwizzler (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (7.13.3):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.13.3)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/Reachability (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- record_darwin (1.0.0):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_contacts:
:path: ".symlinks/plugins/flutter_contacts/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
record_darwin:
:path: ".symlinks/plugins/record_darwin/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS:
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
Firebase: 0312a2352584f782ea56f66d91606891d4607f06
firebase_core: 3b49a055ff54114cae400581c13671fe53936c36
firebase_messaging: 30fa3ec8cd0dc8a860b7817548911b97345a0875
FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_contacts: 5383945387e7ca37cf963d4be57c21f2fc15ca9f
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
nanopb: 438bc412db1928dac798aa6fd75726007be04262
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
PODFILE CHECKSUM: 9de8a3281c07f7034a9eb8ce8a707f95c6003310
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,743 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
28AA274225906B155986CFA0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5EC8ED2BF5CD801902A32493 /* GoogleService-Info.plist */; };
2D5724EF745BE47C385269CC /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 789A4A0733028F1C3A3B8FDF /* libPods-Runner.a */; };
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 */; };
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 */; };
98136D828A733FA3A4A05412 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 70E8C2FE85BB825F63D6FA1F /* libPods-RunnerTests.a */; };
/* 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 */
0C5924099C60D36A9DAB3ABD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
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>"; };
55390CE3750081B409D8F76D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
5EC8ED2BF5CD801902A32493 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
5FAD3FB76264405B9D466D11 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
70E8C2FE85BB825F63D6FA1F /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
789A4A0733028F1C3A3B8FDF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
BE672457097ACB02A0172419 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
C67DFB872FBB6A460051E88E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
F120D236BB7566D5DE73F0B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F90E4DAFC6F0443563808446 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2D5724EF745BE47C385269CC /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C268812A4B18078A4F6BEE93 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
98136D828A733FA3A4A05412 /* libPods-RunnerTests.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
3A897A83E33FFFCB590CAC8E /* Pods */ = {
isa = PBXGroup;
children = (
F90E4DAFC6F0443563808446 /* Pods-Runner.debug.xcconfig */,
F120D236BB7566D5DE73F0B9 /* Pods-Runner.release.xcconfig */,
0C5924099C60D36A9DAB3ABD /* Pods-Runner.profile.xcconfig */,
55390CE3750081B409D8F76D /* Pods-RunnerTests.debug.xcconfig */,
5FAD3FB76264405B9D466D11 /* Pods-RunnerTests.release.xcconfig */,
BE672457097ACB02A0172419 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
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 */,
3A897A83E33FFFCB590CAC8E /* Pods */,
FB95D8457DA48CD0D01718DB /* Frameworks */,
5EC8ED2BF5CD801902A32493 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
C67DFB872FBB6A460051E88E /* Runner.entitlements */,
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>";
};
FB95D8457DA48CD0D01718DB /* Frameworks */ = {
isa = PBXGroup;
children = (
789A4A0733028F1C3A3B8FDF /* libPods-Runner.a */,
70E8C2FE85BB825F63D6FA1F /* libPods-RunnerTests.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
7D88CEB0422F30EB15768526 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
C268812A4B18078A4F6BEE93 /* Frameworks */,
);
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 = (
10D8536383630FFBB49A3FF8 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
55E58A97E5CAD8D638FE10D6 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
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;
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 */,
28AA274225906B155986CFA0 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
10D8536383630FFBB49A3FF8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
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";
};
55E58A97E5CAD8D638FE10D6 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
7D88CEB0422F30EB15768526 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
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;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleqapp.mywhatsapp;
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;
baseConfigurationReference = 55390CE3750081B409D8F76D /* Pods-RunnerTests.debug.xcconfig */;
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.intaleqapp.mywhatsapp.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;
baseConfigurationReference = 5FAD3FB76264405B9D466D11 /* Pods-RunnerTests.release.xcconfig */;
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.intaleqapp.mywhatsapp.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;
baseConfigurationReference = BE672457097ACB02A0172419 /* Pods-RunnerTests.profile.xcconfig */;
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.intaleqapp.mywhatsapp.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 = AppIcon;
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 = AppIcon;
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;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleqapp.mywhatsapp;
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;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.intaleqapp.mywhatsapp;
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 */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<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>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,14 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
GeneratedPluginRegistrant.register(with: self)
return result
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View 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.

View 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>

View 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>

View File

@@ -0,0 +1,30 @@
<?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>API_KEY</key>
<string>AIzaSyDZhiD8lCsi5fa-IB199rY4C3MoYn2X4hQ</string>
<key>GCM_SENDER_ID</key>
<string>146827764316</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.intaleqapp.mywhatsapp</string>
<key>PROJECT_ID</key>
<string>mywhatsapp-inta</string>
<key>STORAGE_BUCKET</key>
<string>mywhatsapp-inta.firebasestorage.app</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:146827764316:ios:c240ccd825bdb66d843c99</string>
</dict>
</plist>

View File

@@ -0,0 +1,61 @@
<?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>Mywhatsapp</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>mywhatsapp</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>NSCameraUsageDescription</key>
<string>This app requires camera access to take and send photos via WhatsApp.</string>
<key>NSContactsUsageDescription</key>
<string>This app requires contacts access to match phone numbers with your local address book names.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to record and send audio messages via WhatsApp.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires photo library access to choose and send photos via WhatsApp.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<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>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -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>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -0,0 +1,6 @@
// import Flutter
// import UIKit
//
// class SceneDelegate: FlutterSceneDelegate {
//
// }

View 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.
}
}

View File

@@ -1,7 +1,7 @@
class AppConfig {
static const String serverHost = "mywhatsappapp.interlap.com";
static const String serverHost = "mywhatsapp.intaleqapp.com";
static const int serverPort = 3025;
static const String wsUrl = "ws://$serverHost:$serverPort";
static const String wsUrl = "wss://$serverHost";
static const int maxReconnectAttempts = 10;
static const Duration reconnectDelay = Duration(seconds: 3);

View File

@@ -1,10 +1,15 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import '../services/whatsapp_service.dart';
import '../models/conversation_model.dart';
import '../models/message_model.dart';
import 'conversations_controller.dart';
class ChatController extends GetxController {
final ConversationModel conversation;
@@ -16,6 +21,13 @@ class ChatController extends GetxController {
final inputCtrl = TextEditingController();
final scrollCtrl = ScrollController();
final hasText = false.obs;
// Recording State
final audioRecord = AudioRecorder();
final isRecording = false.obs;
final recordDuration = 0.obs;
Timer? _recordTimer;
StreamSubscription? _eventSub;
@@ -24,6 +36,17 @@ class ChatController extends GetxController {
@override
void onInit() {
super.onInit();
_svc.activeChatId.value = conversation.id;
// Instantly clear the unread count badge in the UI
try {
Get.find<ConversationsController>().clearUnreadCount(conversation.id);
} catch (_) {}
inputCtrl.addListener(() {
hasText.value = inputCtrl.text.trim().isNotEmpty;
});
loadMessages();
markAsRead();
@@ -33,7 +56,12 @@ class ChatController extends GetxController {
@override
void onClose() {
if (_svc.activeChatId.value == conversation.id) {
_svc.activeChatId.value = null;
}
_eventSub?.cancel();
_recordTimer?.cancel();
audioRecord.dispose();
inputCtrl.dispose();
scrollCtrl.dispose();
super.onClose();
@@ -88,6 +116,35 @@ class ChatController extends GetxController {
}
}
// ── Send Media Message ───────────────────────────────────────────────────
Future<void> sendMediaMessage(String base64, String mimetype, String filename, {String? caption}) async {
isSending.value = true;
try {
final res = await _svc.sendMedia(conversation.id, base64, mimetype, filename, caption: caption);
if (res['type'] == 'message_sent') {
final sentMsg = MessageModel.fromJson(res['data'] as Map<String, dynamic>);
messages.add(sentMsg);
_scrollToBottom();
// Also pre-cache local base64 in mediaCache to display instantly
_svc.mediaCache[sentMsg.id] = base64;
} else {
Get.snackbar('Error', res['message'] ?? 'Failed to send media',
backgroundColor: Colors.redAccent.withOpacity(0.8),
colorText: Colors.white,
);
}
} catch (e) {
print('[SEND MEDIA ERROR] $e');
Get.snackbar('Error', 'Failed to send media: $e',
backgroundColor: Colors.redAccent.withOpacity(0.8),
colorText: Colors.white,
);
} finally {
isSending.value = false;
}
}
// ── Mark Chat as Read ────────────────────────────────────────────────────
Future<void> markAsRead() async {
try {
@@ -124,13 +181,23 @@ class ChatController extends GetxController {
case 'message_ack':
final messageId = event['messageId'] as String?;
final chatId = event['chatId'] as String?;
final ack = event['ack'] as int?;
// ack can arrive as int or double from JSON — handle both
final rawAck = event['ack'];
final ack = rawAck is int
? rawAck
: rawAck is double
? rawAck.toInt()
: null;
if (chatId == null || messageId == null || ack == null) return;
if (chatId == conversation.id) {
final index = messages.indexWhere((m) => m.id == messageId);
if (index != -1) {
messages[index] = messages[index].copyWith(ack: ack);
// Force a list rebuild so Obx re-renders the bubble
final updated = messages[index].copyWith(ack: ack);
final newList = List<MessageModel>.from(messages);
newList[index] = updated;
messages.assignAll(newList);
}
}
break;
@@ -139,15 +206,21 @@ class ChatController extends GetxController {
// ── Helper: Scroll to Bottom ─────────────────────────────────────────────
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) => _performScroll());
// Also trigger after a short delay to account for async layout calculations
Future.delayed(const Duration(milliseconds: 150), () {
_performScroll();
});
}
void _performScroll() {
if (scrollCtrl.hasClients) {
scrollCtrl.animateTo(
scrollCtrl.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
);
}
});
}
// ── Date Separator Logic ─────────────────────────────────────────────────
@@ -182,4 +255,72 @@ class ChatController extends GetxController {
return DateFormat('MMMM d, yyyy').format(dt);
}
}
// ── Audio Recording Engine ───────────────────────────────────────────────
Future<void> startRecording() async {
try {
if (await audioRecord.hasPermission()) {
final tempDir = await getTemporaryDirectory();
final path = '${tempDir.path}/rec_${DateTime.now().millisecondsSinceEpoch}.m4a';
await audioRecord.start(
const RecordConfig(encoder: AudioEncoder.aacLc),
path: path,
);
recordDuration.value = 0;
isRecording.value = true;
_recordTimer?.cancel();
_recordTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
recordDuration.value++;
});
} else {
Get.snackbar(
'Permission Denied',
'Microphone permission is required to record voice notes.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.redAccent.withOpacity(0.8),
colorText: Colors.white,
);
}
} catch (e) {
print('[START RECORDING ERROR] $e');
}
}
Future<void> stopAndSendRecording() async {
try {
_recordTimer?.cancel();
final path = await audioRecord.stop();
isRecording.value = false;
if (path != null && recordDuration.value > 0) {
final file = File(path);
if (await file.exists()) {
final bytes = await file.readAsBytes();
final base64String = base64Encode(bytes);
await sendMediaMessage(
base64String,
'audio/mp4', // Recorded as M4A (AAC), perfect for all platforms natively!
'voice_note.m4a',
);
}
}
} catch (e) {
print('[STOP RECORDING ERROR] $e');
}
}
Future<void> cancelRecording() async {
try {
_recordTimer?.cancel();
await audioRecord.stop();
isRecording.value = false;
recordDuration.value = 0;
} catch (e) {
print('[CANCEL RECORDING ERROR] $e');
}
}
}

View File

@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/whatsapp_service.dart';
import '../services/contacts_service.dart';
import '../models/conversation_model.dart';
class ConversationsController extends GetxController {
@@ -18,6 +21,9 @@ class ConversationsController extends GetxController {
void onInit() {
super.onInit();
// Load local cached conversations first for instant UI response
_loadCachedConversations();
// Load conversations initially if already ready
if (_svc.isWaReady.value) {
loadConversations();
@@ -44,9 +50,61 @@ class ConversationsController extends GetxController {
super.onClose();
}
// Helper to resolve contact names from the local address book
ConversationModel _resolveContactNames(ConversationModel c) {
if (c.isGroup) return c; // Skip group chats
final parts = c.id.split('@');
final phoneNumber = parts[0];
final contactsService = Get.find<ContactsService>();
// Try matching using c.name (which has the formatted number string, e.g. "+962 7 8152 3783")
String matchedName = contactsService.getContactName(c.name, c.name);
// If it didn't match (i.e. returned c.name), try matching using phoneNumber
if (matchedName == c.name) {
matchedName = contactsService.getContactName(phoneNumber, c.name);
}
return c.copyWith(name: matchedName);
}
void clearUnreadCount(String chatId) {
final index = conversations.indexWhere((c) => c.id == chatId);
if (index != -1) {
conversations[index] = conversations[index].copyWith(unreadCount: 0);
_saveConversationsToCache(conversations.map((c) => c.toJson()).toList());
}
}
// ── Local Caching ────────────────────────────────────────────────────────
Future<void> _loadCachedConversations() async {
try {
final prefs = await SharedPreferences.getInstance();
final cached = prefs.getString('cached_conversations');
if (cached != null) {
final List<dynamic> decoded = jsonDecode(cached);
conversations.assignAll(decoded.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
print('[CACHE] Loaded ${conversations.length} conversations instantly.');
}
} catch (e) {
print('[CACHE ERROR] $e');
}
}
Future<void> _saveConversationsToCache(List<dynamic> rawData) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('cached_conversations', jsonEncode(rawData));
print('[CACHE] Saved ${rawData.length} conversations to local cache.');
} catch (e) {
print('[CACHE SAVE ERROR] $e');
}
}
// ── Load Conversations ───────────────────────────────────────────────────
Future<void> loadConversations() async {
if (!_svc.isWaReady.value) return;
if (!_svc.isWaReady.value || isLoading.value) return;
isLoading.value = true;
errorMessage.value = null;
@@ -55,7 +113,8 @@ class ConversationsController extends GetxController {
final res = await _svc.getConversations();
if (res['type'] == 'conversations') {
final List<dynamic> data = res['data'] ?? [];
conversations.assignAll(data.map((c) => ConversationModel.fromJson(c as Map<String, dynamic>)));
conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
_saveConversationsToCache(data);
} else {
errorMessage.value = res['message'] ?? 'Failed to load conversations';
}
@@ -107,16 +166,18 @@ class ConversationsController extends GetxController {
timestamp: msgData['timestamp'] ?? 0,
fromMe: msgData['fromMe'] ?? false,
hasMedia: msgData['hasMedia'] ?? false,
ack: msgData['ack'] ?? 0,
);
// Find existing conversation and update it
final index = conversations.indexWhere((c) => c.id == chatId);
if (index != -1) {
final existing = conversations[index];
final isCurrentActiveChat = _svc.activeChatId.value == chatId;
final updated = existing.copyWith(
lastMessage: lastMsg,
timestamp: lastMsg.timestamp,
unreadCount: lastMsg.fromMe ? existing.unreadCount : existing.unreadCount + 1,
unreadCount: (lastMsg.fromMe || isCurrentActiveChat) ? 0 : existing.unreadCount + 1,
);
conversations.removeAt(index);
conversations.insert(0, updated);
@@ -158,7 +219,8 @@ class ConversationsController extends GetxController {
final res = await _svc.getConversations();
if (res['type'] == 'conversations') {
final List<dynamic> data = res['data'] ?? [];
conversations.assignAll(data.map((c) => ConversationModel.fromJson(c as Map<String, dynamic>)));
conversations.assignAll(data.map((c) => _resolveContactNames(ConversationModel.fromJson(c as Map<String, dynamic>))));
_saveConversationsToCache(data);
}
} catch (_) {}
}

View File

@@ -0,0 +1,86 @@
// 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;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
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.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBwxlFBkdejkm_H5mMAeTK30fuReZ4EeRU',
appId: '1:146827764316:web:99b4d29684ee9a26843c99',
messagingSenderId: '146827764316',
projectId: 'mywhatsapp-inta',
authDomain: 'mywhatsapp-inta.firebaseapp.com',
storageBucket: 'mywhatsapp-inta.firebasestorage.app',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBqCFyXUfd1VjX3htG0Z_-gH4hFOkG31C0',
appId: '1:146827764316:android:75425f8baccb6891843c99',
messagingSenderId: '146827764316',
projectId: 'mywhatsapp-inta',
storageBucket: 'mywhatsapp-inta.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDZhiD8lCsi5fa-IB199rY4C3MoYn2X4hQ',
appId: '1:146827764316:ios:c240ccd825bdb66d843c99',
messagingSenderId: '146827764316',
projectId: 'mywhatsapp-inta',
storageBucket: 'mywhatsapp-inta.firebasestorage.app',
iosBundleId: 'com.intaleqapp.mywhatsapp',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDZhiD8lCsi5fa-IB199rY4C3MoYn2X4hQ',
appId: '1:146827764316:ios:c240ccd825bdb66d843c99',
messagingSenderId: '146827764316',
projectId: 'mywhatsapp-inta',
storageBucket: 'mywhatsapp-inta.firebasestorage.app',
iosBundleId: 'com.intaleqapp.mywhatsapp',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyBwxlFBkdejkm_H5mMAeTK30fuReZ4EeRU',
appId: '1:146827764316:web:8812ccd133ed647e843c99',
messagingSenderId: '146827764316',
projectId: 'mywhatsapp-inta',
authDomain: 'mywhatsapp-inta.firebaseapp.com',
storageBucket: 'mywhatsapp-inta.firebasestorage.app',
);
}

View File

@@ -1,19 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:firebase_core/firebase_core.dart';
import 'services/whatsapp_service.dart';
import 'services/firebase_service.dart';
import 'services/contacts_service.dart';
import 'screens/conversations_screen.dart';
import 'theme/app_theme.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
));
// Register the WhatsApp WebSocket client service before app starts
// Initialize Firebase
try {
await Firebase.initializeApp();
} catch (e) {
print('Firebase initialization error: $e');
}
// Register services before app starts
Get.put(ContactsService(), permanent: true);
Get.put(WhatsAppService(), permanent: true);
Get.put(FirebaseService(), permanent: true);
// Initialize Contacts Service
await Get.find<ContactsService>().init();
Get.find<FirebaseService>().init();
runApp(const WhatsAppApp());
}
@@ -24,9 +36,12 @@ class WhatsAppApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'WhatsApp App',
title: 'WhatsApp',
debugShowCheckedModeBanner: false,
theme: AppTheme.dark,
// Follow device theme — no forced dark/light
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
home: const ConversationsScreen(),
defaultTransition: Transition.cupertino,
);

View File

@@ -3,12 +3,14 @@ class LastMessageModel {
final int timestamp;
final bool fromMe;
final bool hasMedia;
final int ack;
LastMessageModel({
required this.body,
required this.timestamp,
required this.fromMe,
required this.hasMedia,
required this.ack,
});
factory LastMessageModel.fromJson(Map<String, dynamic> json) {
@@ -17,6 +19,7 @@ class LastMessageModel {
timestamp: json['timestamp'] ?? 0,
fromMe: json['fromMe'] ?? false,
hasMedia: json['hasMedia'] ?? false,
ack: json['ack'] ?? 0,
);
}
@@ -26,6 +29,7 @@ class LastMessageModel {
'timestamp': timestamp,
'fromMe': fromMe,
'hasMedia': hasMedia,
'ack': ack,
};
}
}

View File

@@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import '../controllers/chat_controller.dart';
import '../models/conversation_model.dart';
import '../models/message_model.dart';
@@ -17,69 +20,157 @@ class ChatScreen extends StatelessWidget {
ChatController(conversation: conversation),
tag: conversation.id,
);
final isDark = AppTheme.isDark(context);
return Scaffold(
backgroundColor: AppTheme.background,
appBar: _buildAppBar(conversation),
backgroundColor: AppTheme.chatBackground(context),
appBar: _buildAppBar(context, conversation, ctrl),
body: Column(
children: [
Expanded(child: _buildMessageList(ctrl)),
_buildInputBar(ctrl),
Expanded(child: _buildMessageList(context, ctrl)),
_buildInputBar(context, ctrl),
],
),
);
}
AppBar _buildAppBar(ConversationModel chat) => AppBar(
backgroundColor: AppTheme.surface,
leadingWidth: 32,
title: Row(
PreferredSizeWidget _buildAppBar(
BuildContext context,
ConversationModel chat,
ChatController ctrl,
) {
final isDark = AppTheme.isDark(context);
return AppBar(
backgroundColor: AppTheme.surface(context),
leadingWidth: 36,
titleSpacing: 0,
title: InkWell(
onTap: () {}, // Future: open contact info
child: Row(
children: [
_avatar(chat, radius: 18),
_buildAppBarAvatar(context, chat),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
chat.name,
style: const TextStyle(
color: AppTheme.textPrimary,
style: TextStyle(
color: isDark ? AppTheme.darkTextPrimary : Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (chat.isGroup)
const Text(
'Group',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
// Status line
_buildStatusLine(context, chat, ctrl),
],
),
),
],
),
),
actions: [
IconButton(
icon: const Icon(Icons.videocam_outlined, color: AppTheme.iconColor),
icon: Icon(
Icons.videocam_outlined,
color: isDark ? AppTheme.darkTextSecondary : Colors.white,
),
onPressed: null,
),
IconButton(
icon: const Icon(Icons.call_outlined, color: AppTheme.iconColor),
icon: Icon(
Icons.call_outlined,
color: isDark ? AppTheme.darkTextSecondary : Colors.white,
),
onPressed: null,
),
IconButton(
icon: const Icon(Icons.more_vert, color: AppTheme.iconColor),
icon: Icon(
Icons.more_vert,
color: isDark ? AppTheme.darkTextSecondary : Colors.white,
),
onPressed: null,
),
],
);
}
Widget _buildMessageList(ChatController ctrl) {
Widget _buildStatusLine(
BuildContext context,
ConversationModel chat,
ChatController ctrl,
) {
final isDark = AppTheme.isDark(context);
final color = isDark
? AppTheme.darkTextSecondary
: Colors.white.withOpacity(0.85);
if (chat.isGroup) {
return Obx(() {
if (ctrl.isLoading.value) {
return const Center(
final count = ctrl.messages.length;
return Text(
count > 0 ? 'tap for group info' : 'Group',
style: TextStyle(color: color, fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
});
}
// For 1:1 chats, show "online" placeholder or nothing
// (Real status would come from the bridge server)
return Text(
'',
style: TextStyle(color: color, fontSize: 12),
);
}
Widget _buildAppBarAvatar(BuildContext context, ConversationModel chat) {
final isDark = AppTheme.isDark(context);
final fallbackBg = isDark
? const Color(0xff2a3942)
: Colors.white.withOpacity(0.25);
if (chat.avatar != null && chat.avatar!.isNotEmpty) {
return CircleAvatar(
radius: 18,
backgroundColor: fallbackBg,
child: ClipOval(
child: CachedNetworkImage(
imageUrl: chat.avatar!,
width: 36,
height: 36,
fit: BoxFit.cover,
placeholder: (_, __) => _defaultAvatarIcon(chat, fallbackBg),
errorWidget: (_, __, ___) => _defaultAvatarIcon(chat, fallbackBg),
),
),
);
}
return CircleAvatar(
radius: 18,
backgroundColor: fallbackBg,
child: _defaultAvatarIcon(chat, fallbackBg),
);
}
Widget _defaultAvatarIcon(ConversationModel chat, Color bg) {
return Icon(
chat.isGroup ? Icons.group : Icons.person,
color: Colors.white,
size: 20,
);
}
Widget _buildMessageList(BuildContext context, ChatController ctrl) {
return Obx(() {
if (ctrl.isLoading.value && ctrl.messages.isEmpty) {
return Center(
child: CircularProgressIndicator(color: AppTheme.primary),
);
}
@@ -90,11 +181,16 @@ class ChatScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.chat_bubble_outline, color: AppTheme.textSecondary.withOpacity(0.5), size: 48),
Icon(
Icons.chat_bubble_outline,
color: AppTheme.textSecondary(context).withOpacity(0.4),
size: 48,
),
const SizedBox(height: 12),
Text(
'No messages yet',
style: TextStyle(color: AppTheme.textSecondary.withOpacity(0.8)),
style: TextStyle(
color: AppTheme.textSecondary(context).withOpacity(0.8)),
),
],
),
@@ -103,75 +199,183 @@ class ChatScreen extends StatelessWidget {
return ListView.builder(
controller: ctrl.scrollCtrl,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
itemCount: items.length,
itemBuilder: (_, i) {
final item = items[i];
if (item is String) return _buildDateSeparator(item);
if (item is String) return _buildDateSeparator(context, item);
return MessageBubble(message: item as MessageModel);
},
);
});
}
Widget _buildDateSeparator(String label) => Center(
Widget _buildDateSeparator(BuildContext context, String label) {
final isDark = AppTheme.isDark(context);
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
borderRadius: BorderRadius.circular(12),
color: isDark
? const Color(0xff1d2b33)
: const Color(0xffd1f4cc),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Text(
label,
style: const TextStyle(
color: AppTheme.textSecondary,
style: TextStyle(
color: isDark
? AppTheme.darkTextSecondary
: const Color(0xff54656f),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
);
}
Widget _buildInputBar(ChatController ctrl) => Container(
color: AppTheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
Widget _buildInputBar(BuildContext context, ChatController ctrl) {
final isDark = AppTheme.isDark(context);
final barBg = isDark ? AppTheme.darkBackground : AppTheme.lightBackground;
final inputBg = isDark ? AppTheme.darkSurfaceLight : AppTheme.lightSurfaceLight;
return Container(
color: barBg,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: SafeArea(
child: Obx(() {
// ── Recording UI ─────────────────────────────────────────────────
if (ctrl.isRecording.value) {
return Row(
children: [
const SizedBox(width: 12),
const Icon(Icons.fiber_manual_record,
color: Colors.red, size: 14),
const SizedBox(width: 6),
const Text(
'Recording...',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 14),
),
const SizedBox(width: 12),
Text(
'${(ctrl.recordDuration.value ~/ 60).toString().padLeft(2, '0')}:${(ctrl.recordDuration.value % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: AppTheme.textPrimary(context),
fontSize: 14,
fontFeatures: [FontFeature.tabularFigures()]),
),
const Spacer(),
TextButton.icon(
icon: const Icon(Icons.delete,
color: Colors.redAccent, size: 18),
label: const Text('Cancel',
style: TextStyle(color: Colors.redAccent)),
onPressed: ctrl.cancelRecording,
),
const SizedBox(width: 8),
GestureDetector(
onTap: ctrl.stopAndSendRecording,
child: Container(
width: 44,
height: 44,
decoration: const BoxDecoration(
color: AppTheme.primary,
shape: BoxShape.circle,
),
child: const Icon(Icons.check,
color: Colors.white, size: 20),
),
),
const SizedBox(width: 8),
],
);
}
// ── Normal Input ─────────────────────────────────────────────────
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// ── Text input field ─────────────────────────────────────────
Expanded(
child: Container(
decoration: BoxDecoration(
color: inputBg,
borderRadius: BorderRadius.circular(24),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Emoji button
IconButton(
icon: const Icon(Icons.emoji_emotions_outlined, color: AppTheme.iconColor),
icon: Icon(Icons.emoji_emotions_outlined,
color: AppTheme.textSecondary(context)),
onPressed: null,
padding: const EdgeInsets.only(bottom: 2),
),
// Input
Expanded(
child: TextField(
controller: ctrl.inputCtrl,
style: const TextStyle(color: AppTheme.textPrimary),
style: TextStyle(
color: AppTheme.textPrimary(context),
fontSize: 15),
maxLines: 5,
minLines: 1,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: 'Message',
hintStyle: const TextStyle(color: AppTheme.textSecondary),
filled: true,
fillColor: AppTheme.surfaceLight,
hintStyle: TextStyle(
color: AppTheme.textSecondary(context)),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
vertical: 10,
),
),
onSubmitted: (_) => ctrl.sendMessage(),
),
),
// Attachment button
IconButton(
icon: Icon(Icons.attach_file,
color: AppTheme.textSecondary(context)),
onPressed: () => _showAttachmentSheet(context, ctrl),
padding: const EdgeInsets.only(bottom: 2),
),
// Camera button (only when no text)
Obx(() => ctrl.hasText.value
? const SizedBox.shrink()
: IconButton(
icon: Icon(Icons.camera_alt_outlined,
color: AppTheme.textSecondary(context)),
onPressed: () =>
_pickAndSendImage(ctrl, ImageSource.camera),
padding: const EdgeInsets.only(bottom: 2, right: 4),
)),
],
),
),
),
const SizedBox(width: 8),
// Send button
Obx(() => GestureDetector(
onTap: ctrl.sendMessage,
// ── Send / Mic button ─────────────────────────────────────────
GestureDetector(
onTap: () {
if (ctrl.hasText.value) {
ctrl.sendMessage();
} else {
ctrl.startRecording();
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 48,
@@ -182,37 +386,163 @@ class ChatScreen extends StatelessWidget {
),
child: ctrl.isSending.value
? const Padding(
padding: EdgeInsets.all(12),
padding: EdgeInsets.all(13),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.send, color: Colors.white, size: 20),
),
: Obx(() => Icon(
ctrl.hasText.value
? Icons.send_rounded
: Icons.mic_rounded,
color: Colors.white,
size: 22,
)),
),
),
],
);
}),
),
);
}
void _showAttachmentSheet(BuildContext context, ChatController ctrl) {
final isDark = AppTheme.isDark(context);
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: isDark ? AppTheme.darkSurface : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: AppTheme.textSecondary(context).withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildAttachmentItem(
context: context,
icon: Icons.photo_library_rounded,
color: const Color(0xff7c4dff),
label: 'Gallery',
onTap: () {
Get.back();
_pickAndSendImage(ctrl, ImageSource.gallery);
},
),
_buildAttachmentItem(
context: context,
icon: Icons.camera_alt_rounded,
color: const Color(0xffff4081),
label: 'Camera',
onTap: () {
Get.back();
_pickAndSendImage(ctrl, ImageSource.camera);
},
),
_buildAttachmentItem(
context: context,
icon: Icons.insert_drive_file_rounded,
color: const Color(0xff2196f3),
label: 'Document',
onTap: () => Get.back(),
),
_buildAttachmentItem(
context: context,
icon: Icons.mic_rounded,
color: const Color(0xffff9800),
label: 'Audio',
onTap: () {
Get.back();
ctrl.startRecording();
},
),
],
),
],
),
),
);
Widget _avatar(ConversationModel chat, {double radius = 24}) {
if (chat.avatar != null) {
return CircleAvatar(
radius: radius,
backgroundImage: NetworkImage(chat.avatar!),
backgroundColor: AppTheme.surfaceLight,
barrierColor: Colors.black.withOpacity(0.5),
);
}
return CircleAvatar(
radius: radius,
backgroundColor: AppTheme.primaryDark,
child: Text(
chat.name.isNotEmpty ? chat.name[0].toUpperCase() : '?',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
void _pickAndSendImage(ChatController ctrl, ImageSource source) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: source,
imageQuality: 75,
);
if (image == null) return;
final bytes = await image.readAsBytes();
final base64String = base64Encode(bytes);
String mimetype = 'image/jpeg';
if (image.path.toLowerCase().endsWith('.png')) {
mimetype = 'image/png';
} else if (image.path.toLowerCase().endsWith('.gif')) {
mimetype = 'image/gif';
}
await ctrl.sendMediaMessage(
base64String,
mimetype,
image.name,
);
} catch (e) {
Get.snackbar(
'Error picking image',
e.toString(),
backgroundColor: Colors.redAccent.withOpacity(0.8),
colorText: Colors.white,
);
}
}
Widget _buildAttachmentItem({
required BuildContext context,
required IconData icon,
required Color color,
required String label,
required VoidCallback onTap,
}) {
final isDark = AppTheme.isDark(context);
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 54,
height: 54,
decoration: BoxDecoration(
color: color.withOpacity(0.12),
shape: BoxShape.circle,
),
child: Icon(icon, color: color, size: 26),
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
color: AppTheme.textPrimary(context), fontSize: 12),
),
],
),
);
}

View File

@@ -15,134 +15,218 @@ class ConversationsScreen extends StatelessWidget {
Widget build(BuildContext context) {
final svc = Get.find<WhatsAppService>();
final ctrl = Get.put(ConversationsController());
final isDark = AppTheme.isDark(context);
return Scaffold(
backgroundColor: AppTheme.background,
appBar: _buildAppBar(ctrl),
backgroundColor: AppTheme.background(context),
appBar: _buildAppBar(context, ctrl),
body: Obx(() {
// Not connected
if (svc.status.value == WsStatus.disconnected ||
svc.status.value == WsStatus.connecting) {
return _buildConnecting();
return _buildConnecting(context);
}
// QR Code needed
if (svc.qrData.value != null) {
return const QrView();
}
// Loading conversations
if (ctrl.isLoading.value) {
return const Center(
if (ctrl.isLoading.value && ctrl.conversations.isEmpty) {
return Center(
child: CircularProgressIndicator(color: AppTheme.primary),
);
}
// Error
if (ctrl.errorMessage.value != null) {
return _buildError(ctrl);
if (ctrl.errorMessage.value != null && ctrl.conversations.isEmpty) {
return _buildError(context, ctrl);
}
// Empty
if (ctrl.conversations.isEmpty) {
return _buildEmpty();
return _buildEmpty(context);
}
// List
return _buildList(ctrl);
return _buildList(context, ctrl);
}),
);
}
AppBar _buildAppBar(ConversationsController ctrl) {
PreferredSizeWidget _buildAppBar(
BuildContext context, ConversationsController ctrl) {
final searching = false.obs;
final isDark = AppTheme.isDark(context);
return AppBar(
backgroundColor: AppTheme.surface,
backgroundColor: AppTheme.surface(context),
elevation: 0,
title: Obx(() => searching.value
? TextField(
autofocus: true,
style: const TextStyle(color: AppTheme.textPrimary),
decoration: const InputDecoration(
style: TextStyle(color: isDark ? Colors.white : Colors.white),
cursorColor: Colors.white,
decoration: InputDecoration(
hintText: 'Search...',
border: InputBorder.none,
hintStyle: TextStyle(color: AppTheme.textSecondary),
hintStyle: TextStyle(
color: Colors.white.withOpacity(0.7)),
),
onChanged: ctrl.search,
)
: const Text('WhatsApp', style: TextStyle(color: AppTheme.textPrimary))),
: const Text(
'WhatsApp',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
)),
actions: [
Obx(() => IconButton(
icon: Icon(
searching.value ? Icons.close : Icons.search,
color: AppTheme.iconColor,
color: isDark ? AppTheme.darkTextSecondary : Colors.white,
),
onPressed: () {
searching.value = !searching.value;
if (!searching.value) ctrl.loadConversations();
},
)),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: AppTheme.iconColor),
color: AppTheme.surface,
onSelected: (v) {
if (v == 'refresh') ctrl.loadConversations();
},
itemBuilder: (_) => [
const PopupMenuItem(
value: 'refresh',
child: Text('Refresh', style: TextStyle(color: AppTheme.textPrimary)),
IconButton(
icon: Icon(
Icons.more_vert,
color: isDark ? AppTheme.darkTextSecondary : Colors.white,
),
onPressed: () => _showOptionsMenu(context, ctrl),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(
height: 1,
color: isDark
? Colors.white.withOpacity(0.08)
: Colors.white.withOpacity(0.15),
),
),
);
}
void _showOptionsMenu(
BuildContext context, ConversationsController ctrl) {
showMenu(
context: context,
position: const RelativeRect.fromLTRB(1000, 56, 0, 0),
color: AppTheme.isDark(context)
? AppTheme.darkSurface
: Colors.white,
items: [
PopupMenuItem(
onTap: ctrl.loadConversations,
child: Text(
'Refresh',
style: TextStyle(color: AppTheme.textPrimary(context)),
),
),
],
);
}
Widget _buildConnecting() => Center(
Widget _buildConnecting(BuildContext context) {
final svc = Get.find<WhatsAppService>();
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: AppTheme.primary),
CircularProgressIndicator(color: AppTheme.primary),
const SizedBox(height: 16),
Text(
'Connecting to server...',
style: TextStyle(color: AppTheme.textSecondary),
style: TextStyle(color: AppTheme.textSecondary(context)),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
svc.status.value = WsStatus.disconnected;
svc.connect();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary,
),
child: const Text(
'Retry Now',
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
Widget _buildError(ConversationsController ctrl) => Center(
Widget _buildError(
BuildContext context, ConversationsController ctrl) =>
Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.redAccent, size: 48),
const Icon(Icons.error_outline,
color: Colors.redAccent, size: 48),
const SizedBox(height: 12),
Text(
ctrl.errorMessage.value ?? 'Error',
style: const TextStyle(color: AppTheme.textSecondary),
style: TextStyle(
color: AppTheme.textSecondary(context)),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: ctrl.loadConversations,
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primary),
child: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primary),
child: const Text('Retry',
style: TextStyle(color: Colors.white)),
),
],
),
),
);
Widget _buildEmpty(BuildContext context) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.chat_bubble_outline,
size: 64,
color:
AppTheme.textSecondary(context).withOpacity(0.4)),
const SizedBox(height: 16),
Text(
'No conversations yet',
style: TextStyle(
color: AppTheme.textSecondary(context),
fontSize: 16),
),
],
),
);
Widget _buildEmpty() => const Center(
child: Text(
'No conversations found',
style: TextStyle(color: AppTheme.textSecondary),
),
);
Widget _buildList(ConversationsController ctrl) {
Widget _buildList(
BuildContext context, ConversationsController ctrl) {
return RefreshIndicator(
color: AppTheme.primary,
backgroundColor: AppTheme.surface,
backgroundColor: AppTheme.isDark(context)
? AppTheme.darkSurface
: Colors.white,
onRefresh: ctrl.loadConversations,
child: ListView.builder(
child: ListView.separated(
itemCount: ctrl.conversations.length,
separatorBuilder: (_, __) => Divider(
height: 1,
thickness: 0.5,
indent: 76,
color: AppTheme.isDark(context)
? Colors.white.withOpacity(0.06)
: Colors.black.withOpacity(0.08),
),
itemBuilder: (_, i) {
final chat = ctrl.conversations[i];
return ConversationTile(

View File

@@ -20,40 +20,41 @@ class QrView extends StatelessWidget {
const Icon(Icons.qr_code_scanner,
color: AppTheme.primary, size: 64),
const SizedBox(height: 16),
const Text(
Text(
'Link with your phone',
style: TextStyle(
color: AppTheme.textPrimary,
color: AppTheme.textPrimary(context),
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: AppTheme.surfaceLight,
color: AppTheme.surfaceLight(context),
borderRadius: BorderRadius.circular(8),
),
child: const Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'1. Open WhatsApp on your phone',
style:
TextStyle(color: AppTheme.textSecondary, fontSize: 14),
style: TextStyle(
color: AppTheme.textSecondary(context), fontSize: 14),
),
SizedBox(height: 4),
const SizedBox(height: 4),
Text(
'2. Tap Menu (⋮ or ⚙️) → Linked Devices',
style:
TextStyle(color: AppTheme.textSecondary, fontSize: 14),
style: TextStyle(
color: AppTheme.textSecondary(context), fontSize: 14),
),
SizedBox(height: 4),
const SizedBox(height: 4),
Text(
'3. Tap "Link a Device" and scan this QR code',
style:
TextStyle(color: AppTheme.textSecondary, fontSize: 14),
style: TextStyle(
color: AppTheme.textSecondary(context), fontSize: 14),
),
],
),
@@ -66,13 +67,21 @@ class QrView extends StatelessWidget {
}
try {
final base64Image = qr.contains(',') ? qr.split(',')[1] : qr;
final base64Image =
qr.contains(',') ? qr.split(',')[1] : qr;
final bytes = base64Decode(base64Image);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 12,
offset: const Offset(0, 4),
)
],
),
child: Image.memory(
bytes,
@@ -89,7 +98,8 @@ class QrView extends StatelessWidget {
const SizedBox(height: 8),
Text(
'Failed to render QR Code: $e',
style: const TextStyle(color: AppTheme.textSecondary),
style: TextStyle(
color: AppTheme.textSecondary(context)),
),
],
);
@@ -98,7 +108,8 @@ class QrView extends StatelessWidget {
const SizedBox(height: 16),
Text(
'Waiting for QR Code from WhatsApp...',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
style: TextStyle(
color: AppTheme.textSecondary(context), fontSize: 12),
),
],
),

View File

@@ -0,0 +1,85 @@
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';
class ContactInfo {
final String name;
final String? avatarPath;
ContactInfo({required this.name, this.avatarPath});
}
class ContactsService extends GetxService {
final RxMap<String, ContactInfo> _contactsMap = <String, ContactInfo>{}.obs;
final RxBool permissionGranted = false.obs;
Future<ContactsService> init() async {
await fetchContacts();
return this;
}
Future<void> fetchContacts() async {
try {
// Check and request permission
bool permission = await FlutterContacts.requestPermission(readonly: true);
permissionGranted.value = permission;
if (permission) {
// Fetch contacts with photos and phone numbers
final contacts = await FlutterContacts.getContacts(withProperties: true, withPhoto: true);
final Map<String, ContactInfo> tempMap = {};
for (var contact in contacts) {
final fullName = contact.displayName;
if (fullName.isEmpty) continue;
for (var phone in contact.phones) {
final normalized = normalizePhoneNumber(phone.number);
if (normalized.isNotEmpty) {
tempMap[normalized] = ContactInfo(
name: fullName,
avatarPath: null, // Custom local avatar path can be handled if needed
);
}
}
}
_contactsMap.assignAll(tempMap);
print('[CONTACTS] Successfully loaded ${_contactsMap.length} normalized phone contacts');
}
} catch (e) {
print('[CONTACTS ERROR] Failed to fetch system contacts: $e');
}
}
// Normalizes numbers to match them easily (e.g., removes spaces, dashes, brackets, and leading zeros)
String normalizePhoneNumber(String number) {
String clean = number.replaceAll(RegExp(r'[\s\-\(\)\+]'), '');
// If it starts with local country prefix or leading 0, we can do substring matches
if (clean.startsWith('00')) {
clean = clean.substring(2);
}
return clean;
}
String getContactName(String rawNumber, String fallback) {
if (!permissionGranted.value || _contactsMap.isEmpty) return fallback;
final clean = normalizePhoneNumber(rawNumber);
if (clean.isEmpty) return fallback;
// Direct match
if (_contactsMap.containsKey(clean)) {
return _contactsMap[clean]!.name;
}
// Partial match for varying country codes (match last 9 digits of the phone number)
if (clean.length >= 9) {
final suffix = clean.substring(clean.length - 9);
for (var key in _contactsMap.keys) {
if (key.endsWith(suffix)) {
return _contactsMap[key]!.name;
}
}
}
return fallback;
}
}

View File

@@ -0,0 +1,231 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import '../controllers/conversations_controller.dart';
import '../models/conversation_model.dart';
import '../screens/chat_screen.dart';
import 'whatsapp_service.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// We can't access GetX services easily here since it's a separate isolate,
// but Firebase automatically shows the notification.
print("Handling a background message: ${message.messageId}");
}
class FirebaseService extends GetxService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Future<FirebaseService> init() async {
// 1. Request Permission
await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
// 2. Initialize Local Notifications (for foreground)
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings();
const initSettings =
InitializationSettings(android: androidInit, iOS: iosInit);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTap,
);
// 3. Background Handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 4. Foreground Message Handler
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final chatId = message.data['chatId'];
final activeChatId = Get.find<WhatsAppService>().activeChatId.value;
// Smart Notification: Only show if we are NOT currently in this chat
if (chatId != null && activeChatId == chatId) {
print('[FCM] Silent notification, user is currently in chat $chatId');
return; // Silent
}
_showLocalNotification(message);
});
// 5. Handle tapping on a background notification when app opens
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleNotificationClick(message.data);
});
// 6. Check if app was opened from a terminated state via a notification
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
Future.delayed(const Duration(seconds: 1), () {
_handleNotificationClick(initialMessage.data);
});
}
// 7. Request and register FCM Token on the server
_setupFcmTokenRegistration();
return this;
}
void _setupFcmTokenRegistration() async {
try {
// For iOS, wait until the APNS token is successfully initialized by the OS before calling getToken()
if (Platform.isIOS) {
String? apnsToken = await _messaging.getAPNSToken();
int retries = 0;
while (apnsToken == null && retries < 15) {
print('[FCM] APNS token not set yet. Waiting 1 second... (Attempt ${retries + 1}/15)');
await Future.delayed(const Duration(seconds: 1));
apnsToken = await _messaging.getAPNSToken();
retries++;
}
if (apnsToken != null) {
print('[FCM] APNS Token successfully obtained: $apnsToken');
} else {
print('[FCM WARNING] Failed to obtain APNS token after 15 attempts. FCM registration might fail.');
}
}
final token = await _messaging.getToken();
if (token != null) {
final displayToken = token.length > 15 ? token.substring(0, 15) : token;
print('[FCM] Token obtained: $displayToken...');
_registerTokenOnServer(token);
}
} catch (e) {
print('[FCM ERROR] Could not get token: $e');
}
_messaging.onTokenRefresh.listen((newToken) {
print('[FCM] Token refreshed');
_registerTokenOnServer(newToken);
});
}
void _registerTokenOnServer(String token) {
final wsSvc = Get.find<WhatsAppService>();
if (wsSvc.status.value == WsStatus.waReady ||
wsSvc.status.value == WsStatus.connected) {
print('[FCM] Sending token to server...');
wsSvc.sendFcmToken(token);
} else {
// Listen to status changes and send once connected
late StreamSubscription sub;
sub = wsSvc.status.listen((status) {
if (status == WsStatus.waReady || status == WsStatus.connected) {
print('[FCM] WebSocket connected, sending token to server...');
wsSvc.sendFcmToken(token);
sub.cancel();
}
});
}
}
void _showLocalNotification(RemoteMessage message) {
final notification = message.notification;
if (notification == null) return;
const androidDetails = AndroidNotificationDetails(
'whatsapp_channel',
'WhatsApp Messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails();
const details =
NotificationDetails(android: androidDetails, iOS: iosDetails);
_localNotifications.show(
notification.hashCode,
notification.title,
notification.body,
details,
payload: jsonEncode(message.data),
);
}
void showLocalNotificationFromData(Map<String, dynamic> data) {
final chatId = data['chatId'];
final name = data['name'] ?? 'WhatsApp';
final body = data['body'] ?? 'New Message';
// Only show local notifications when the app is actively in the foreground (resumed).
// If the app is in the background or suspended, the native FCM notification will handle it natively.
// This fully prevents duplicate notifications!
if (WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) {
print('[FCM] Skipping WebSocket local notification: app is in background.');
return;
}
// Smart Notification: Only show if we are NOT currently in this chat
final activeChatId = Get.find<WhatsAppService>().activeChatId.value;
if (chatId != null && activeChatId == chatId) {
return; // Silent
}
// Do not show local notification if the chat is muted in user's conversations list
if (chatId != null && Get.isRegistered<ConversationsController>()) {
final controller = Get.find<ConversationsController>();
final index = controller.conversations.indexWhere((c) => c.id == chatId);
if (index != -1 && controller.conversations[index].isMuted) {
print('[LOCAL NOTIF] Skipping: Chat $chatId is muted by user.');
return; // Silent
}
}
const androidDetails = AndroidNotificationDetails(
'whatsapp_channel',
'WhatsApp Messages',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
_localNotifications.show(
DateTime.now().microsecond,
name,
body,
details,
payload: jsonEncode({'chatId': chatId, 'name': name}),
);
}
void _onNotificationTap(NotificationResponse response) {
if (response.payload != null) {
final data = jsonDecode(response.payload!);
_handleNotificationClick(data);
}
}
void _handleNotificationClick(Map<String, dynamic> data) {
final chatId = data['chatId'];
final name = data['name'] ?? 'Chat';
if (chatId != null) {
final dummyChat = ConversationModel(
id: chatId,
name: name,
isGroup: false,
unreadCount: 0,
timestamp: 0,
pinned: false,
isMuted: false,
);
Get.to(() => ChatScreen(conversation: dummyChat), preventDuplicates: false);
}
}
}

View File

@@ -3,6 +3,8 @@ import 'dart:convert';
import 'package:get/get.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../config/app_config.dart';
import 'contacts_service.dart';
import 'firebase_service.dart';
enum WsStatus { disconnected, connecting, connected, waReady }
@@ -11,6 +13,7 @@ class WhatsAppService extends GetxService {
final status = WsStatus.disconnected.obs;
final qrData = Rx<String?>(null);
final isWaReady = false.obs;
final activeChatId = Rx<String?>(null);
// ── Internal ─────────────────────────────────────────────────────────────
WebSocketChannel? _channel;
@@ -50,6 +53,7 @@ class WhatsAppService extends GetxService {
status.value = WsStatus.connecting;
_reconnectTimer?.cancel();
print('[WS] Attempting to connect to ${AppConfig.wsUrl}...');
try {
_channel = WebSocketChannel.connect(Uri.parse(AppConfig.wsUrl));
@@ -61,15 +65,18 @@ class WhatsAppService extends GetxService {
);
status.value = WsStatus.connected;
_reconnectCount = 0;
print('[WS] Connected successfully.');
// Request initial status check
ping();
} catch (e) {
print('[WS] Connection exception: $e');
_scheduleReconnect();
}
}
void _onData(dynamic raw) {
print('[WS RECV] $raw');
Map<String, dynamic> data;
try {
data = jsonDecode(raw as String);
@@ -88,7 +95,34 @@ class WhatsAppService extends GetxService {
// Push events
switch (type) {
case 'new_message':
// Trigger a local notification if the app is open (WebSocket connected)
final chatId = data['chatId'];
final msgData = data['data'];
if (msgData != null && msgData['fromMe'] != true) {
String body = msgData['body'] ?? '';
if (body.isEmpty && msgData['hasMedia'] == true) {
body = '📷 Media/Audio message';
}
try {
final String cleanNumber = chatId?.split('@')[0] ?? '';
final String senderName = Get.find<ContactsService>().getContactName(
cleanNumber,
cleanNumber.isNotEmpty ? '+$cleanNumber' : 'WhatsApp',
);
Get.find<FirebaseService>().showLocalNotificationFromData({
'chatId': chatId,
'name': senderName,
'body': body,
});
} catch (e) {
print('[LOCAL NOTIF ERROR] $e');
}
}
break;
case 'qr':
qrData.value = data['qr'];
isWaReady.value = false;
if (status.value == WsStatus.waReady) {
@@ -126,10 +160,12 @@ class WhatsAppService extends GetxService {
}
void _onError(Object err) {
print('[WS ERROR] $err');
_handleDisconnect();
}
void _onDone() {
print('[WS DONE] Connection closed by server');
_handleDisconnect();
}
@@ -146,12 +182,14 @@ class WhatsAppService extends GetxService {
void _scheduleReconnect() {
_reconnectTimer?.cancel();
if (_reconnectCount >= AppConfig.maxReconnectAttempts) {
print('[WS] Max reconnect attempts reached');
return;
}
_reconnectCount++;
_reconnectTimer = Timer(AppConfig.reconnectDelay, connect);
// Progressive backoff: starting at reconnectDelay (3s) up to 15s maximum
final delaySec = (AppConfig.reconnectDelay.inSeconds * (_reconnectCount > 5 ? 5 : _reconnectCount)).clamp(3, 15);
final delay = Duration(seconds: delaySec);
print('[WS] Reconnecting in ${delay.inSeconds} seconds (attempt $_reconnectCount)...');
_reconnectTimer = Timer(delay, connect);
}
// ── Request/Response ─────────────────────────────────────────────────────
@@ -168,16 +206,18 @@ class WhatsAppService extends GetxService {
_pending[id] = completer;
try {
_channel?.sink.add(jsonEncode(payload));
final jsonMsg = jsonEncode(payload);
print('[WS SEND] $jsonMsg');
_channel?.sink.add(jsonMsg);
} catch (e) {
_pending.remove(id);
completer.completeError(e);
return completer.future;
}
// Timeout after 15s
// Timeout after 60s
return completer.future.timeout(
const Duration(seconds: 15),
const Duration(seconds: 60),
onTimeout: () {
_pending.remove(id);
throw TimeoutException('Request timed out: ${payload['type']}');
@@ -203,6 +243,41 @@ class WhatsAppService extends GetxService {
Future<Map<String, dynamic>> searchConversations(String query) =>
_request({ 'type': 'search_conversations', 'query': query });
Future<Map<String, dynamic>> sendFcmToken(String token) =>
_request({ 'type': 'register_fcm', 'token': token });
Future<Map<String, dynamic>> getMedia(String messageId) =>
_request({ 'type': 'get_media', 'messageId': messageId });
Future<Map<String, dynamic>> sendMedia(String chatId, String base64, String mimetype, String filename, {String? caption}) =>
_request({
'type': 'send_media',
'chatId': chatId,
'base64': base64,
'mimetype': mimetype,
'filename': filename,
'caption': caption ?? ''
});
// Cache downloaded media: messageId -> base64
final RxMap<String, String> mediaCache = <String, String>{}.obs;
Future<String?> downloadAndCacheMedia(String messageId) async {
if (mediaCache.containsKey(messageId)) return mediaCache[messageId];
try {
final res = await getMedia(messageId);
if (res['type'] == 'media' && res['data'] != null) {
final String base64Data = res['data'];
mediaCache[messageId] = base64Data;
return base64Data;
}
} catch (e) {
print('[MEDIA DOWNLOAD ERROR] Failed to download message media: $e');
}
return null;
}
Future<Map<String, dynamic>> ping() =>
_request({ 'type': 'ping' });
}

View File

@@ -1,39 +1,56 @@
import 'package:flutter/material.dart';
class AppTheme {
// Dark WhatsApp Palette
static const Color background = Color(0xff111b21);
static const Color surface = Color(0xff1f2c34);
static const Color surfaceLight = Color(0xff2a3942);
static const Color primary = Color(0xff00a884);
static const Color primaryDark = Color(0xff005c4b);
// ── WhatsApp Dark Palette ────────────────────────────────────────────────
static const Color darkBackground = Color(0xff111b21);
static const Color darkSurface = Color(0xff1f2c34);
static const Color darkSurfaceLight = Color(0xff2a3942);
static const Color darkOutgoingMsg = Color(0xff005c4b);
static const Color darkIncomingMsg = Color(0xff1f2c34);
static const Color darkTextPrimary = Color(0xffe9edef);
static const Color darkTextSecondary= Color(0xff8696a0);
static const Color outgoingMsg = Color(0xff005c4b);
static const Color incomingMsg = Color(0xff1f2c34);
// ── WhatsApp Light Palette ───────────────────────────────────────────────
static const Color lightBackground = Color(0xffffffff);
static const Color lightSurface = Color(0xff075e54); // WhatsApp green header
static const Color lightSurfaceLight = Color(0xfff0f2f5);
static const Color lightOutgoingMsg = Color(0xffd9fdd3);
static const Color lightIncomingMsg = Color(0xffffffff);
static const Color lightTextPrimary = Color(0xff111b21);
static const Color lightTextSecondary= Color(0xff667781);
static const Color lightChatBg = Color(0xffe5ddd5); // WhatsApp chat wallpaper bg
static const Color textPrimary = Color(0xffe9edef);
static const Color textSecondary = Color(0xff8696a0);
static const Color iconColor = Color(0xff8696a0);
// ── Shared Colors ────────────────────────────────────────────────────────
static const Color primary = Color(0xff25d366); // WhatsApp green
static const Color primaryDark = Color(0xff128c7e);
static const Color teal = Color(0xff075e54);
static const Color blueTick = Color(0xff53bdeb); // WhatsApp blue double tick
static const Color greyTick = Color(0xff667781);
// ── Dark Theme ───────────────────────────────────────────────────────────
static ThemeData get dark {
return ThemeData.dark().copyWith(
scaffoldBackgroundColor: background,
primaryColor: primary,
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: darkBackground,
primaryColor: teal,
colorScheme: const ColorScheme.dark(
primary: primary,
background: background,
surface: surface,
secondary: primaryDark,
surface: darkSurface,
background: darkBackground,
),
appBarTheme: const AppBarTheme(
backgroundColor: surface,
backgroundColor: darkSurface,
foregroundColor: darkTextPrimary,
elevation: 0,
iconTheme: IconThemeData(color: iconColor),
iconTheme: IconThemeData(color: darkTextSecondary),
titleTextStyle: TextStyle(
color: textPrimary,
color: darkTextPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
dividerColor: darkSurfaceLight,
textSelectionTheme: const TextSelectionThemeData(
cursorColor: primary,
selectionColor: primaryDark,
@@ -41,4 +58,70 @@ class AppTheme {
),
);
}
// ── Light Theme ──────────────────────────────────────────────────────────
static ThemeData get light {
return ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: lightBackground,
primaryColor: teal,
colorScheme: const ColorScheme.light(
primary: teal,
secondary: primary,
surface: lightSurface,
background: lightBackground,
),
appBarTheme: const AppBarTheme(
backgroundColor: teal,
foregroundColor: Colors.white,
elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
dividerColor: Color(0xffe0e0e0),
textSelectionTheme: const TextSelectionThemeData(
cursorColor: teal,
selectionColor: primary,
selectionHandleColor: teal,
),
);
}
// ── Context-aware helpers ─────────────────────────────────────────────────
static bool isDark(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
static Color background(BuildContext context) =>
isDark(context) ? darkBackground : lightBackground;
static Color surface(BuildContext context) =>
isDark(context) ? darkSurface : lightSurface;
static Color surfaceLight(BuildContext context) =>
isDark(context) ? darkSurfaceLight : lightSurfaceLight;
static Color outgoingMsg(BuildContext context) =>
isDark(context) ? darkOutgoingMsg : lightOutgoingMsg;
static Color incomingMsg(BuildContext context) =>
isDark(context) ? darkIncomingMsg : lightIncomingMsg;
static Color chatBackground(BuildContext context) =>
isDark(context) ? darkBackground : lightChatBg;
static Color textPrimary(BuildContext context) =>
isDark(context) ? darkTextPrimary : lightTextPrimary;
static Color textSecondary(BuildContext context) =>
isDark(context) ? darkTextSecondary : lightTextSecondary;
static Color iconColor(BuildContext context) =>
isDark(context) ? darkTextSecondary : Colors.white;
static Color subtitleIconColor(BuildContext context) =>
isDark(context) ? darkTextSecondary : lightTextSecondary;
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../models/conversation_model.dart';
import '../theme/app_theme.dart';
@@ -17,117 +18,217 @@ class ConversationTile extends StatelessWidget {
Widget build(BuildContext context) {
final lastMsg = conversation.lastMessage;
final hasUnread = conversation.unreadCount > 0;
final isDark = AppTheme.isDark(context);
return ListTile(
return InkWell(
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: _buildAvatar(),
title: Row(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
// ── Avatar ──────────────────────────────────────────────────────
_buildAvatar(context, conversation),
const SizedBox(width: 12),
// ── Content ─────────────────────────────────────────────────────
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name row + time
Row(
children: [
Expanded(
child: Text(
conversation.name,
style: const TextStyle(
color: AppTheme.textPrimary,
fontSize: 16,
style: TextStyle(
color: AppTheme.textPrimary(context),
fontSize: 16.5,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
const SizedBox(width: 6),
Text(
_formatTime(conversation.timestamp),
style: TextStyle(
color: hasUnread ? AppTheme.primary : AppTheme.textSecondary,
color: hasUnread
? AppTheme.primary
: AppTheme.textSecondary(context),
fontSize: 12,
fontWeight: hasUnread ? FontWeight.bold : FontWeight.normal,
fontWeight:
hasUnread ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
const SizedBox(height: 4),
// Subtitle row: ack icon + preview + badges
Row(
children: [
// ── ACK icon for sent messages ───────────────────────
if (lastMsg != null && lastMsg.fromMe) ...[
const Icon(Icons.done_all, size: 16, color: AppTheme.primary), // Or proper ACK double tick
const SizedBox(width: 4),
_buildAckIcon(context, lastMsg.ack),
const SizedBox(width: 3),
],
// ── Message preview ──────────────────────────────────
Expanded(
child: Text(
_getSubtitleText(lastMsg),
style: const TextStyle(
color: AppTheme.textSecondary,
_getSubtitleText(context, lastMsg),
style: TextStyle(
color: AppTheme.textSecondary(context),
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// ── Trailing badges ──────────────────────────────────
if (conversation.isMuted) ...[
const SizedBox(width: 8),
const Icon(Icons.volume_off, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 4),
Icon(Icons.volume_off,
size: 15, color: AppTheme.textSecondary(context)),
],
if (conversation.pinned) ...[
const SizedBox(width: 8),
const Icon(Icons.push_pin, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 4),
Icon(Icons.push_pin,
size: 15, color: AppTheme.textSecondary(context)),
],
if (hasUnread) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.all(6),
const SizedBox(width: 6),
_buildUnreadBadge(conversation.unreadCount),
],
],
),
],
),
),
],
),
),
);
}
// ── Avatar builder (cached network image + fallback initials) ─────────────
Widget _buildAvatar(BuildContext context, ConversationModel c) {
final isDark = AppTheme.isDark(context);
final fallbackBg =
isDark ? const Color(0xff2a3942) : const Color(0xff6b7c85);
if (c.avatar != null && c.avatar!.isNotEmpty) {
return CircleAvatar(
radius: 28,
backgroundColor: fallbackBg,
child: ClipOval(
child: CachedNetworkImage(
imageUrl: c.avatar!,
width: 56,
height: 56,
fit: BoxFit.cover,
placeholder: (_, __) => _initialsAvatar(c.name, fallbackBg),
errorWidget: (_, __, ___) => _initialsAvatar(c.name, fallbackBg),
),
),
);
}
// Group icon or person icon
if (c.isGroup) {
return CircleAvatar(
radius: 28,
backgroundColor: fallbackBg,
child: const Icon(Icons.group, color: Colors.white, size: 30),
);
}
return CircleAvatar(
radius: 28,
backgroundColor: fallbackBg,
child: _initialsAvatar(c.name, fallbackBg),
);
}
Widget _initialsAvatar(String name, Color bg) {
return Container(
width: 56,
height: 56,
color: bg,
alignment: Alignment.center,
child: Icon(
Icons.person,
color: Colors.white,
size: 30,
),
);
}
// ── Unread badge ──────────────────────────────────────────────────────────
Widget _buildUnreadBadge(int count) {
return Container(
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: const BoxDecoration(
color: AppTheme.primary,
shape: BoxShape.circle,
),
child: Text(
conversation.unreadCount.toString(),
style: const TextStyle(
color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
),
);
}
Widget _buildAvatar() {
if (conversation.avatar != null) {
return CircleAvatar(
radius: 26,
backgroundImage: NetworkImage(conversation.avatar!),
backgroundColor: AppTheme.surfaceLight,
);
}
return CircleAvatar(
radius: 26,
backgroundColor: AppTheme.primaryDark,
child: Text(
conversation.name.isNotEmpty ? conversation.name[0].toUpperCase() : '?',
count > 99 ? '99+' : count.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
String _getSubtitleText(LastMessageModel? lastMsg) {
// ── ACK (delivery status) icon ────────────────────────────────────────────
// Real WhatsApp ACK levels from whatsapp-web.js:
// -1 = error → clock (pending/error)
// 0 = pending → clock
// 1 = sent → single grey tick
// 2 = received → double grey tick
// 3 = read/played→ double blue tick
Widget _buildAckIcon(BuildContext context, int ack) {
switch (ack) {
case -1:
case 0:
// Pending / clock
return Icon(Icons.access_time_rounded,
size: 14, color: AppTheme.textSecondary(context));
case 1:
// Sent — single grey tick
return Icon(Icons.check_rounded,
size: 15, color: AppTheme.textSecondary(context));
case 2:
// Delivered — double grey tick
return Icon(Icons.done_all_rounded,
size: 15, color: AppTheme.textSecondary(context));
case 3:
// Read — double blue tick
return const Icon(Icons.done_all_rounded,
size: 15, color: AppTheme.blueTick);
default:
return const SizedBox.shrink();
}
}
// ── Subtitle text ─────────────────────────────────────────────────────────
String _getSubtitleText(BuildContext context, LastMessageModel? lastMsg) {
if (lastMsg == null) return '';
if (lastMsg.hasMedia) {
return '📷 Photo'; // or other media indicator
return '📷 Photo';
}
return lastMsg.body;
}
// ── Time formatter ────────────────────────────────────────────────────────
String _formatTime(int timestamp) {
if (timestamp == 0) return '';
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
@@ -137,13 +238,13 @@ class ConversationTile extends StatelessWidget {
final msgDate = DateTime(dt.year, dt.month, dt.day);
if (msgDate == today) {
return DateFormat('hh:mm a').format(dt);
return DateFormat('h:mm a').format(dt);
} else if (msgDate == yesterday) {
return 'Yesterday';
} else if (now.difference(dt).inDays < 7) {
return DateFormat('EEEE').format(dt); // e.g. "Monday"
return DateFormat('EEEE').format(dt);
} else {
return DateFormat('MM/dd/yy').format(dt);
return DateFormat('dd/MM/yy').format(dt);
}
}
}

View File

@@ -1,8 +1,50 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:get/get.dart';
import 'package:audioplayers/audioplayers.dart';
import '../models/message_model.dart';
import '../theme/app_theme.dart';
import '../services/whatsapp_service.dart';
// ─── Full-Screen Image Viewer ─────────────────────────────────────────────
class FullScreenImageViewer extends StatelessWidget {
final Uint8List bytes;
final String heroTag;
const FullScreenImageViewer({
super.key,
required this.bytes,
required this.heroTag,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
),
body: Center(
child: Hero(
tag: heroTag,
child: InteractiveViewer(
minScale: 0.5,
maxScale: 5.0,
child: Image.memory(bytes, fit: BoxFit.contain),
),
),
),
);
}
}
// ─── Message Bubble ───────────────────────────────────────────────────────
class MessageBubble extends StatelessWidget {
final MessageModel message;
@@ -11,156 +53,170 @@ class MessageBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isMe = message.fromMe;
final align = isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start;
final bg = isMe ? AppTheme.outgoingMsg : AppTheme.incomingMsg;
final isDark = AppTheme.isDark(context);
final bg = isMe
? AppTheme.outgoingMsg(context)
: AppTheme.incomingMsg(context);
final radius = isMe
? const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(0),
topRight: Radius.circular(4),
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
)
: const BorderRadius.only(
topLeft: Radius.circular(0),
topLeft: Radius.circular(4),
topRight: Radius.circular(12),
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
);
return Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
crossAxisAlignment: align,
children: [
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
// Incoming message shadow/border in light mode
final BoxDecoration decoration = BoxDecoration(
color: bg,
borderRadius: radius,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.2 : 0.08),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
);
return Container(
margin: EdgeInsets.only(
top: 2,
bottom: 2,
left: isMe ? 60 : 8,
right: isMe ? 8 : 60,
),
child: Column(
crossAxisAlignment:
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// Tail + bubble
Stack(
children: [
// Message bubble
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.78,
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: decoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Show sender name in group chats if not from me
// Sender name in group chats (incoming only)
if (!isMe && message.author != null) ...[
Text(
message.author!,
style: const TextStyle(
color: AppTheme.primary,
fontSize: 12,
color: AppTheme.primaryDark,
fontSize: 12.5,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
],
// Media
if (message.hasMedia) ...[
InteractiveMediaWidget(message: message),
const SizedBox(height: 4),
],
// Media placeholder if it is media
if (message.hasMedia) ...[
_buildMediaPlaceholder(),
const SizedBox(height: 6),
// Text + time + ACK row
_buildTextTimeRow(context, isMe),
],
),
),
],
),
],
),
);
}
// Message text & time row
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
Widget _buildTextTimeRow(BuildContext context, bool isMe) {
// If body is empty (media-only message), just show time+ack
final hasBody = message.body.trim().isNotEmpty;
if (!hasBody && message.hasMedia) {
// Show only time+ack at bottom right of media
return Align(
alignment: Alignment.bottomRight,
child: _timeAckRow(context, isMe),
);
}
return Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.end,
children: [
Flexible(
child: Text(
if (hasBody)
Text(
message.body,
style: const TextStyle(
color: AppTheme.textPrimary,
style: TextStyle(
color: AppTheme.textPrimary(context),
fontSize: 15,
),
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
const SizedBox(width: 4),
_timeAckRow(context, isMe),
],
);
}
Widget _timeAckRow(BuildContext context, bool isMe) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatTime(message.timestamp),
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 10,
style: TextStyle(
color: AppTheme.textSecondary(context),
fontSize: 11,
),
),
if (isMe) ...[
const SizedBox(width: 4),
_buildAckIcon(message.ack),
const SizedBox(width: 3),
_buildAckIcon(context, message.ack),
],
],
),
),
],
),
],
),
),
],
),
);
}
Widget _buildMediaPlaceholder() {
IconData iconData = Icons.insert_drive_file;
String label = "File Attachment";
switch (message.type) {
case "image":
iconData = Icons.photo;
label = "Image";
break;
case "video":
iconData = Icons.videocam;
label = "Video";
break;
case "audio":
iconData = Icons.audiotrack;
label = "Audio File";
break;
case "sticker":
iconData = Icons.emoji_emotions;
label = "Sticker";
break;
}
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(iconData, color: AppTheme.textSecondary, size: 32),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(color: AppTheme.textPrimary, fontWeight: FontWeight.w500),
),
],
),
);
}
Widget _buildAckIcon(int ack) {
// ── ACK icon ──────────────────────────────────────────────────────────────
// Real WhatsApp ACK values from whatsapp-web.js:
// -1 = error
// 0 = pending (clock icon)
// 1 = sent (single grey tick ✓)
// 2 = delivered/received (double grey tick ✓✓)
// 3 = read/played (double BLUE tick ✓✓)
Widget _buildAckIcon(BuildContext context, int ack) {
switch (ack) {
case 1: // Pending
return const Icon(Icons.access_time, size: 13, color: AppTheme.textSecondary);
case 2: // Sent
return const Icon(Icons.done, size: 15, color: AppTheme.textSecondary);
case 3: // Delivered
return const Icon(Icons.done_all, size: 15, color: AppTheme.textSecondary);
case 4: // Read
return const Icon(Icons.done_all, size: 15, color: Colors.blue);
case -1:
case 0:
// Pending — clock
return Icon(Icons.access_time_rounded,
size: 14, color: AppTheme.textSecondary(context));
case 1:
// Sent — single grey tick
return Icon(Icons.check_rounded,
size: 16, color: AppTheme.textSecondary(context));
case 2:
// Delivered — double grey tick
return Icon(Icons.done_all_rounded,
size: 16, color: AppTheme.textSecondary(context));
case 3:
// Read — double blue tick
return const Icon(Icons.done_all_rounded,
size: 16, color: AppTheme.blueTick);
default:
return const SizedBox.shrink();
}
@@ -172,3 +228,371 @@ class MessageBubble extends StatelessWidget {
return DateFormat('h:mm a').format(dt);
}
}
// ─── Interactive Media Widget ─────────────────────────────────────────────
class InteractiveMediaWidget extends StatefulWidget {
final MessageModel message;
const InteractiveMediaWidget({super.key, required this.message});
@override
State<InteractiveMediaWidget> createState() => _InteractiveMediaWidgetState();
}
class _InteractiveMediaWidgetState extends State<InteractiveMediaWidget> {
final WhatsAppService _svc = Get.find<WhatsAppService>();
bool _isLoading = false;
// Audio player state
final AudioPlayer _player = AudioPlayer();
StreamSubscription? _posSub;
StreamSubscription? _durSub;
StreamSubscription? _stateSub;
bool _isPlaying = false;
double _audioProgress = 0.0;
int _audioDurationSeconds = 1;
int _audioCurrentSeconds = 0;
@override
void initState() {
super.initState();
_posSub = _player.onPositionChanged.listen((p) {
if (mounted) {
setState(() {
_audioCurrentSeconds = p.inSeconds;
if (_audioDurationSeconds > 0) {
_audioProgress = p.inMilliseconds / (_audioDurationSeconds * 1000);
if (_audioProgress > 1.0) _audioProgress = 1.0;
}
});
}
});
_durSub = _player.onDurationChanged.listen((d) {
if (mounted) {
setState(() {
_audioDurationSeconds = d.inSeconds > 0 ? d.inSeconds : 1;
});
}
});
_stateSub = _player.onPlayerStateChanged.listen((s) {
if (mounted) {
setState(() {
_isPlaying = s == PlayerState.playing;
if (s == PlayerState.completed) {
_audioProgress = 0.0;
_audioCurrentSeconds = 0;
_isPlaying = false;
}
});
}
});
}
@override
void dispose() {
_posSub?.cancel();
_durSub?.cancel();
_stateSub?.cancel();
_player.dispose();
super.dispose();
}
void _toggleAudioPlayback(String base64Data) async {
try {
if (_isPlaying) {
await _player.pause();
} else {
final bytes = base64Decode(base64Data);
final safeId =
widget.message.id.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '');
final tempDir = Directory.systemTemp;
final tempFile = File('${tempDir.path}/voice_$safeId.mp3');
if (!await tempFile.exists()) {
await tempFile.writeAsBytes(bytes);
}
await _player.play(DeviceFileSource(tempFile.path));
}
} catch (e) {
print('[AUDIO PLAYBACK ERROR] $e');
Get.snackbar(
'Playback Error',
'Could not play audio message: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.redAccent.withOpacity(0.8),
colorText: Colors.white,
);
}
}
@override
Widget build(BuildContext context) {
return Obx(() {
final cachedMedia = _svc.mediaCache[widget.message.id];
if (cachedMedia != null) {
return _buildDownloadedMedia(context, cachedMedia);
}
if (_isLoading) {
return Container(
padding: const EdgeInsets.all(16),
width: 140,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.12),
borderRadius: BorderRadius.circular(8),
),
child: const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2, color: AppTheme.primary),
),
);
}
// Tap to download placeholder
return GestureDetector(
onTap: _downloadMedia,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.10),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primary.withOpacity(0.15),
shape: BoxShape.circle,
),
child: Icon(_getIcon(),
color: AppTheme.primary, size: 22),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getLabel(),
style: TextStyle(
color: AppTheme.textPrimary(context),
fontWeight: FontWeight.w500,
fontSize: 13),
),
const SizedBox(height: 2),
Row(
children: [
Icon(Icons.download_rounded,
size: 12,
color: AppTheme.textSecondary(context)),
const SizedBox(width: 3),
Text(
'Tap to download',
style: TextStyle(
color: AppTheme.textSecondary(context),
fontSize: 11),
),
],
),
],
),
],
),
),
);
});
}
Future<void> _downloadMedia() async {
setState(() => _isLoading = true);
await _svc.downloadAndCacheMedia(widget.message.id);
if (mounted) {
setState(() => _isLoading = false);
}
}
Widget _buildDownloadedMedia(BuildContext context, String base64Data) {
final bytes = base64Decode(base64Data);
// ── Image / Sticker ────────────────────────────────────────────────────
if (widget.message.type == "image" || widget.message.type == "sticker") {
final heroTag = 'img_${widget.message.id}';
return GestureDetector(
onTap: () {
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
barrierColor: Colors.black,
pageBuilder: (_, __, ___) => FullScreenImageViewer(
bytes: bytes,
heroTag: heroTag,
),
transitionsBuilder: (_, anim, __, child) =>
FadeTransition(opacity: anim, child: child),
),
);
},
child: Hero(
tag: heroTag,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 260),
child: Image.memory(
bytes,
fit: BoxFit.cover,
width: double.infinity,
),
),
),
),
);
}
// ── Audio / Voice Note ─────────────────────────────────────────────────
if (widget.message.type == "audio") {
final totalSec = _audioDurationSeconds > 1 ? _audioDurationSeconds : _audioCurrentSeconds;
final durationStr =
'${(totalSec ~/ 60).toString().padLeft(1, '0')}:${(totalSec % 60).toString().padLeft(2, '0')}';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 18,
backgroundColor: AppTheme.primary,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
_isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
color: Colors.white,
size: 20,
),
onPressed: () => _toggleAudioPlayback(base64Data),
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 2,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 5),
overlayShape:
const RoundSliderOverlayShape(overlayRadius: 10),
activeTrackColor: AppTheme.primary,
inactiveTrackColor:
AppTheme.textSecondary(context).withOpacity(0.3),
thumbColor: AppTheme.primary,
overlayColor: AppTheme.primary.withOpacity(0.2),
),
child: Slider(
value: _audioProgress.clamp(0.0, 1.0),
onChanged: (v) async {
final targetMs =
(v * _audioDurationSeconds * 1000).toInt();
await _player
.seek(Duration(milliseconds: targetMs));
},
),
),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
durationStr,
style: TextStyle(
color: AppTheme.textSecondary(context),
fontSize: 11,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
),
],
),
),
],
),
);
}
// ── Default: Document / File ───────────────────────────────────────────
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.10),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primary.withOpacity(0.15),
shape: BoxShape.circle,
),
child: const Icon(Icons.insert_drive_file_rounded,
color: AppTheme.primary, size: 22),
),
const SizedBox(width: 10),
Text(
_getLabel(),
style: TextStyle(
color: AppTheme.textPrimary(context),
fontWeight: FontWeight.w500,
fontSize: 13),
),
],
),
);
}
IconData _getIcon() {
switch (widget.message.type) {
case "image":
return Icons.photo_camera_rounded;
case "video":
return Icons.videocam_rounded;
case "audio":
return Icons.mic_rounded;
case "sticker":
return Icons.emoji_emotions_rounded;
default:
return Icons.insert_drive_file_rounded;
}
}
String _getLabel() {
switch (widget.message.type) {
case "image":
return "Photo";
case "video":
return "Video";
case "audio":
return "Voice note";
case "sticker":
return "Sticker";
default:
return "File";
}
}
}

1
whatsapp_app/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

Some files were not shown because too many files have changed in this diff Show More