Commit 38200824 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Pre-reset backup: full Flutter app + .claude config + all assets

parents
{
"permissions": {
"allow": [
"Bash(*)",
"Read(*)",
"Edit(*)",
"Write(*)",
"Agent(*)",
"Workflow(*)",
"WebFetch(*)",
"NotebookEdit(*)"
],
"defaultMode": "bypassPermissions"
}
}
# 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
# Signing
android/key.properties
android/app/upload-keystore.jks
*.jks
*.keystore
# 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: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: android
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: ios
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
# 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'
This diff is collapsed.
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9j4m//cEMsGH9fWDxlS8KxNrKO42Yn+ZdBEPC0JcCXnNkt5v
slGreclTJzbUZ3PIDo8xTY3S41PdkXJuqblvlOVBfcGup/26MPsVtX2RuQXJxpSq
EiM69yM0wYD3JH/Rsv19YUDhOq799T571IluU5JBCLPxI7rjHeCFJRb6b741+aRo
d/O/XVU1mp8SBbPOufbKfFE6HaDh34XBaleDgaEtvLTNMxJeJ0tMFBrXymTjn0Et
Xv184A+puYQ0JTaEFxYrhemvZJxE7JX2IH2DeNdfOoZs1AyS634LK9CsK8bdu+bK
MKeMPFweyP5NZDwG0Fau+UK902a1V3Ts0nHytwIDAQABAoIBAH14ugKfaYI7Xv4T
NiwvLRp98JKQnyGdhyG/N30zORaS/A8kTKH+P3P1A+vtYlm8g2n3yNWNuLGhIDtm
pcDR/mtOTgq4UDjFbsRajHtIGqK+skv/oJFRZlBbnhwjjMsKXl3oEKUnnBx84ma+
ifaIeLEYyw6WMf9U0Tl+GBoIFWAt+g/j0sRzQlP/YiBWpJVXLaBZUnZvkvNAQH/r
AGcxfJWMFMZ4Gr/goGcyyKm5I43jmZH3pQFe3LtQq8GpMDPCegl3tuMpz66L62u1
XhDpZrc6JMhmGHrAhL6SOb21CXnpD4uXFZThcmRpsBAVjAVch4OnOTcVd1onANCo
7LL7IDECgYEA+3yUxAQbpYF1f9aVLsE0jAgZQi9JVdXzYAdeGFXxzouEQ+jc2ZWs
3oQ+hkD1Tbo80uGSZ25YN/0XuQby2LIzkqdLNeaD+mkD6OrQiIlq600Cd7fpMzTN
F6Vc3zzPznwMKyIJrQmHvI1bgeDXbORNiuI8/d/uMOzZEp1TSewoyI0CgYEA+ql6
o9o7TxzqMjveTo/Le/qnpjNJt5SiuwwO0exCOujz8Yz4zIiQoChadifEGBORFD1v
jTAq30eJS6z230CEiBMwWQ9U4ZTEDl43uwfIb5kBQpA46nDMpeOJaYIAZZ0Q5hyk
RNE17Pv79rF+9YNYrszyW7o8QtGfSnKMPswJ4VMCgYEA+o7Z7R2mqdP8zDxv7+BA
yU98Uuun5iQ+0aslfZuLSlzIj5xfBU3RqPUbEkl/3UwzM2vYoMJYlJfN3ePlai7J
oVCaZ35eecvNQG9LCu3Wn87fKYYLiqANwoRXrnb5F+GRghvshgEbwqkXxaPNHkms
VDYpzEsiFo6zi49Bn0HyNDUCgYBZmyRgVq9FBM0j0AsUgor3cw9jdoovuk7CFvll
SdQTQxuRiheW5mrtFf/gpS6QfM4sMhoimsl4sBYAm4BVJrc5/cIW1Eg/q/K6QCwk
DBA29LdimDQAevptgv2oXfTOXmugFzUR9MgWQ6467hC4q4+UTWeGKvlH2a/b6T8B
Y+dUSQKBgGP6zuQZxvFLDXiLFbjALnYkGW3y4AcbksfGS81s8+QnEKzDFaxcu/6M
d4Ww5X1WVNbTNBA3ea49zuV1snp+fC158EvSexHzGVQdPylH0VqPte4OOK7deyDj
XFVhIkhrrdhyfmaOfioXvHIInFt/hTlujFVkkpl5WX7PHmBLKdej
-----END RSA PRIVATE KEY-----
\ No newline at end of file
# el3ab
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.
# 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
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
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.alarcade.el3ab"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "com.alarcade.el3ab"
minSdk = 24
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled = true
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {
source = "../.."
}
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**
<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>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
android:label="EL3AB"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="false"
android:hardwareAccelerated="true">
<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">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Main launcher -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep Links (custom scheme) -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="el3ab"/>
</intent-filter>
<!-- App Links (HTTPS universal links) -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="el3ab-player.caprover.al-arcade.com"/>
</intent-filter>
</activity>
<!-- Notification channel default -->
<meta-data
android:name="default_notification_channel_id"
android:value="el3ab_game"/>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
</manifest>
package com.alarcade.el3ab
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
<?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>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/launch_bg" />
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">#FF0A1020</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">#FF050810</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">#FF0A1020</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="notification_color">#FFE4AC38</color>
<color name="launch_bg">#FF050810</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">#FF0A1020</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">#FF050810</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">#FF0A1020</item>
</style>
</resources>
<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>
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)
}
This source diff could not be displayed because it is too large. You can view the blob instead.
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
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
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
**/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
<?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>
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
platform :ios, '15.0'
# 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!
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)
end
end
PODS:
- app_links (6.4.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.4.8):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- OrderedSet
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
Flutter:
:path: Flutter
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: ce2a4dd764e1c7aeed6a7cdc5e61d092b6dc6d32
COCOAPODS: 1.16.2
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
<?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>
<?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>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?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>
<?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>
<?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>
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Register for local notifications
UNUserNotificationCenter.current().delegate = self
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
{
"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"
}
}
{
"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"
}
}
# 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.
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" 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="21678"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleAspectFit" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
<constraints>
<constraint firstAttribute="width" constant="150" id="w-constraint"/>
<constraint firstAttribute="height" constant="150" id="h-constraint"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" red="0.019607843137254902" green="0.031372549019607843" blue="0.062745098039215685" 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>
<?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>
<?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>El3ab - العب</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>El3ab</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/>
<!-- Deep Links -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.al-arcade.el3ab</string>
<key>CFBundleURLSchemes</key>
<array>
<string>el3ab</string>
</array>
</dict>
</array>
<!-- Universal Links -->
<key>FlutterDeepLinkingEnabled</key>
<true/>
<!-- Camera usage -->
<key>NSCameraUsageDescription</key>
<string>نحتاج للكاميرا لتحديث صورة بروفايلك</string>
<!-- Photo library -->
<key>NSPhotoLibraryUsageDescription</key>
<string>نحتاج للصور لاختيار صورة بروفايلك</string>
<!-- Contacts -->
<key>NSContactsUsageDescription</key>
<string>نحتاج لجهات الاتصال لمساعدتك بإيجاد أصدقائك على EL3AB</string>
<!-- Biometric -->
<key>NSFaceIDUsageDescription</key>
<string>استخدم Face ID لتسجيل دخول سريع</string>
<!-- Microphone (for potential future voice chat) -->
<key>NSMicrophoneUsageDescription</key>
<string>نحتاج المايكروفون للمحادثات الصوتية</string>
<!-- Background modes for push -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>
#import "GeneratedPluginRegistrant.h"
<?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>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:el3ab-player.caprover.al-arcade.com</string>
</array>
</dict>
</plist>
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
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.
}
}
import 'package:flutter/material.dart';
import 'config/theme.dart';
import 'core/services/auth_service.dart';
import 'core/services/connectivity_service.dart';
import 'core/services/deep_link_service.dart';
import 'core/services/push_service.dart';
import 'features/shell/app_shell.dart';
import 'features/splash/splash_screen.dart';
class El3abApp extends StatefulWidget {
final AuthService authService;
final PushService pushService;
final DeepLinkService deepLinkService;
final ConnectivityService connectivityService;
const El3abApp({
super.key,
required this.authService,
required this.pushService,
required this.deepLinkService,
required this.connectivityService,
});
@override
State<El3abApp> createState() => _El3abAppState();
}
class _El3abAppState extends State<El3abApp> {
bool _showSplash = true;
String? _biometricToken;
void _onSplashComplete() {
setState(() => _showSplash = false);
}
void _onBiometricSuccess() async {
final token = await widget.authService.getToken();
setState(() {
_biometricToken = token;
_showSplash = false;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'EL3AB',
debugShowCheckedModeBanner: false,
theme: El3abTheme.dark,
locale: const Locale('ar'),
builder: (context, child) {
return Directionality(
textDirection: TextDirection.rtl,
child: child!,
);
},
home: _showSplash
? SplashScreen(
authService: widget.authService,
onComplete: _onSplashComplete,
onBiometricSuccess: _onBiometricSuccess,
)
: AppShell(
authService: widget.authService,
pushService: widget.pushService,
deepLinkService: widget.deepLinkService,
connectivityService: widget.connectivityService,
initialToken: _biometricToken,
),
);
}
}
class AppConstants {
AppConstants._();
static const String appName = 'EL3AB';
static const String packageName = 'com.al-arcade.el3ab';
// Supabase
static const String supabaseUrl = 'https://safe-supabase-kong.caprover.al-arcade.com';
static const String supabaseAnonKey =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84';
// Web player URL
static const String webPlayerUrl = 'https://el3ab-player.caprover.al-arcade.com/';
static const String webPlayerHost = 'el3ab-player.caprover.al-arcade.com';
// Allowed domains for WebView navigation
static const List<String> allowedDomains = [
// App & backend
'el3ab-player.caprover.al-arcade.com',
'al-arcade.com',
'safe-supabase-kong.caprover.al-arcade.com',
'stockfishapi.caprover.al-arcade.com',
'swissapi.caprover.al-arcade.com',
// Google OAuth
'accounts.google.com',
'google.com',
'googleapis.com',
'gstatic.com',
'googleusercontent.com',
// Apple Sign-In (future)
'appleid.apple.com',
'apple.com',
// Facebook OAuth (future)
'facebook.com',
'fbcdn.net',
// CDN & assets
'cloudflare.com',
'cdn.jsdelivr.net',
'fonts.googleapis.com',
'fonts.gstatic.com',
// Payment providers (future)
'stripe.com',
'js.stripe.com',
];
// Deep link schemes
static const String deepLinkScheme = 'el3ab';
static const String universalLinkHost = 'el3ab-player.caprover.al-arcade.com';
// Storage keys
static const String authTokenKey = 'auth_token';
static const String refreshTokenKey = 'refresh_token';
static const String userIdKey = 'user_id';
static const String biometricEnabledKey = 'biometric_enabled';
static const String onboardingCompleteKey = 'onboarding_complete';
static const String pushTokenKey = 'push_token';
// Timeouts
static const Duration webViewTimeout = Duration(seconds: 30);
static const Duration backgroundGracePeriod = Duration(minutes: 5);
}
import 'package:flutter/material.dart';
class El3abColors {
El3abColors._();
// Backgrounds
static const Color bgDeep = Color(0xFF050810);
static const Color bgBase = Color(0xFF0A1020);
static const Color bgCard = Color(0xFF121A2E);
static const Color bgElevated = Color(0xFF1A2440);
// Brand
static const Color gold = Color(0xFFE4AC38);
static const Color goldSoft = Color(0xFFFFCC66);
static const Color cyan = Color(0xFF00FFFF);
static const Color blue = Color(0xFF2082F0);
// Game colors
static const Color chessPrimary = Color(0xFF2563EB);
static const Color ludoPrimary = Color(0xFF8B5CF6);
static const Color dominoPrimary = Color(0xFF10B981);
static const Color backgammonPrimary = Color(0xFFF59E0B);
// Text
static const Color textPrimary = Color(0xFFF8FAFC);
static const Color textSecondary = Color(0xFF94A3B8);
static const Color textMuted = Color(0xFF475569);
// Status
static const Color success = Color(0xFF34D399);
static const Color error = Color(0xFFEF4444);
static const Color warning = Color(0xFFF59E0B);
// Border
static const Color border = Color(0x0FFFFFFF);
}
class El3abTheme {
El3abTheme._();
static ThemeData get dark => ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: El3abColors.bgDeep,
colorScheme: const ColorScheme.dark(
primary: El3abColors.gold,
secondary: El3abColors.cyan,
surface: El3abColors.bgBase,
error: El3abColors.error,
),
fontFamily: 'IBM Plex Sans Arabic',
appBarTheme: const AppBarTheme(
backgroundColor: El3abColors.bgBase,
elevation: 0,
centerTitle: true,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: El3abColors.bgBase,
indicatorColor: El3abColors.gold.withValues(alpha: 0.15),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: El3abColors.gold,
fontFamily: 'IBM Plex Sans Arabic',
);
}
return const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: El3abColors.textSecondary,
fontFamily: 'IBM Plex Sans Arabic',
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: El3abColors.gold, size: 26);
}
return const IconThemeData(color: El3abColors.textSecondary, size: 24);
}),
),
);
}
enum BridgeCommand {
haptic,
share,
audio,
navSync,
authReady,
authLogout,
badge,
cacheProfile,
cacheMatch,
requestContacts,
requestCamera,
}
class BridgeMessage {
final BridgeCommand command;
final Map<String, dynamic> data;
BridgeMessage({required this.command, this.data = const {}});
factory BridgeMessage.fromJson(Map<String, dynamic> json) {
final cmd = _parseCommand(json['cmd'] as String? ?? '');
final data = Map<String, dynamic>.from(json)..remove('cmd');
return BridgeMessage(command: cmd, data: data);
}
static BridgeCommand _parseCommand(String cmd) {
switch (cmd) {
case 'haptic':
return BridgeCommand.haptic;
case 'share':
return BridgeCommand.share;
case 'audio':
return BridgeCommand.audio;
case 'nav_sync':
return BridgeCommand.navSync;
case 'auth_ready':
return BridgeCommand.authReady;
case 'auth_logout':
return BridgeCommand.authLogout;
case 'badge':
return BridgeCommand.badge;
case 'cache_profile':
return BridgeCommand.cacheProfile;
case 'cache_match':
return BridgeCommand.cacheMatch;
case 'request_contacts':
return BridgeCommand.requestContacts;
case 'request_camera':
return BridgeCommand.requestCamera;
default:
return BridgeCommand.haptic;
}
}
}
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'bridge_commands.dart';
typedef BridgeHandler = void Function(BridgeMessage message);
class JsBridge {
InAppWebViewController? _controller;
final Map<BridgeCommand, List<BridgeHandler>> _handlers = {};
void attach(InAppWebViewController controller) {
_controller = controller;
}
void detach() {
_controller = null;
}
void on(BridgeCommand command, BridgeHandler handler) {
_handlers.putIfAbsent(command, () => []).add(handler);
}
void off(BridgeCommand command, BridgeHandler handler) {
_handlers[command]?.remove(handler);
}
void handleMessage(String rawMessage) {
try {
final json = jsonDecode(rawMessage) as Map<String, dynamic>;
final message = BridgeMessage.fromJson(json);
final handlers = _handlers[message.command];
if (handlers != null) {
for (final handler in handlers) {
handler(message);
}
}
} catch (e) {
debugPrint('[JsBridge] Error parsing message: $e');
}
}
Future<void> sendToWeb(String method, Map<String, dynamic> data) async {
if (_controller == null) return;
final jsonData = jsonEncode(data);
final js = 'if(window.el3ab_native&&window.el3ab_native.$method){window.el3ab_native.$method($jsonData)}';
try {
await _controller!.evaluateJavascript(source: js);
} catch (e) {
debugPrint('[JsBridge] Error sending to web: $e');
}
}
Future<void> onDeepLink(String route, String id) async {
await sendToWeb('onDeepLink', {'route': route, 'id': id});
}
Future<void> onPushTap(Map<String, dynamic> payload) async {
await sendToWeb('onPushTap', payload);
}
Future<void> onConnectivityChanged(bool online) async {
await sendToWeb('onConnectivityChanged', {'online': online});
}
Future<void> onTokenRefreshed(String token) async {
await sendToWeb('onTokenRefreshed', {'token': token});
}
Future<void> onNavTap(String world) async {
await sendToWeb('onNavTap', {'world': world});
}
}
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/constants.dart';
class AuthService {
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
final LocalAuthentication _localAuth = LocalAuthentication();
Future<bool> get isBiometricAvailable async {
try {
final canCheck = await _localAuth.canCheckBiometrics;
final isSupported = await _localAuth.isDeviceSupported();
return canCheck && isSupported;
} catch (_) {
return false;
}
}
Future<bool> get isBiometricEnabled async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(AppConstants.biometricEnabledKey) ?? false;
}
Future<void> setBiometricEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(AppConstants.biometricEnabledKey, enabled);
}
Future<bool> authenticateWithBiometric() async {
try {
return await _localAuth.authenticate(
localizedReason: 'سجّل دخولك باستخدام البصمة',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
} catch (_) {
return false;
}
}
Future<void> saveToken(String token) async {
await _secureStorage.write(key: AppConstants.authTokenKey, value: token);
}
Future<void> saveRefreshToken(String token) async {
await _secureStorage.write(key: AppConstants.refreshTokenKey, value: token);
}
Future<void> saveUserId(String userId) async {
await _secureStorage.write(key: AppConstants.userIdKey, value: userId);
}
Future<String?> getToken() async {
return _secureStorage.read(key: AppConstants.authTokenKey);
}
Future<String?> getRefreshToken() async {
return _secureStorage.read(key: AppConstants.refreshTokenKey);
}
Future<String?> getUserId() async {
return _secureStorage.read(key: AppConstants.userIdKey);
}
Future<bool> hasStoredCredentials() async {
final token = await getToken();
return token != null && token.isNotEmpty;
}
Future<void> clearCredentials() async {
await _secureStorage.delete(key: AppConstants.authTokenKey);
await _secureStorage.delete(key: AppConstants.refreshTokenKey);
await _secureStorage.delete(key: AppConstants.userIdKey);
await setBiometricEnabled(false);
}
}
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
class ConnectivityService {
final Connectivity _connectivity = Connectivity();
final StreamController<bool> _controller = StreamController<bool>.broadcast();
bool _isOnline = true;
Stream<bool> get onConnectivityChanged => _controller.stream;
bool get isOnline => _isOnline;
Future<void> init() async {
final results = await _connectivity.checkConnectivity();
_isOnline = !results.contains(ConnectivityResult.none);
_connectivity.onConnectivityChanged.listen((results) {
final online = !results.contains(ConnectivityResult.none);
if (online != _isOnline) {
_isOnline = online;
_controller.add(online);
}
});
}
void dispose() {
_controller.close();
}
}
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/foundation.dart';
class DeepLinkData {
final String route;
final String? id;
DeepLinkData({required this.route, this.id});
}
class DeepLinkService {
final AppLinks _appLinks = AppLinks();
final StreamController<DeepLinkData> _linkController =
StreamController<DeepLinkData>.broadcast();
Stream<DeepLinkData> get links => _linkController.stream;
Future<void> init() async {
// Handle initial link (app opened via link)
try {
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_handleUri(initialUri);
}
} catch (e) {
debugPrint('[DeepLink] No initial link: $e');
}
// Handle subsequent links (app already running)
_appLinks.uriLinkStream.listen(
_handleUri,
onError: (e) => debugPrint('[DeepLink] Stream error: $e'),
);
}
void _handleUri(Uri uri) {
// Handle el3ab://match/abc123 or https://el3ab-player.../match/abc123
final segments = uri.pathSegments;
if (segments.isEmpty) return;
final route = segments.first;
final id = segments.length > 1 ? segments[1] : null;
_linkController.add(DeepLinkData(route: route, id: id));
}
void dispose() {
_linkController.close();
}
}
import 'package:flutter/services.dart';
class HapticsService {
void trigger(String type) {
switch (type) {
case 'light':
HapticFeedback.lightImpact();
break;
case 'medium':
HapticFeedback.mediumImpact();
break;
case 'heavy':
HapticFeedback.heavyImpact();
break;
case 'success':
HapticFeedback.mediumImpact();
Future.delayed(const Duration(milliseconds: 100), () {
HapticFeedback.lightImpact();
});
break;
case 'error':
HapticFeedback.heavyImpact();
Future.delayed(const Duration(milliseconds: 80), () {
HapticFeedback.heavyImpact();
});
break;
case 'selection':
HapticFeedback.selectionClick();
break;
case 'dice':
_dicePattern();
break;
case 'coin':
_coinPattern();
break;
default:
HapticFeedback.lightImpact();
}
}
void _dicePattern() async {
for (int i = 0; i < 3; i++) {
HapticFeedback.selectionClick();
await Future.delayed(const Duration(milliseconds: 60));
}
}
void _coinPattern() async {
for (int i = 0; i < 3; i++) {
HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 40));
}
}
}
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class PushService {
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
RealtimeChannel? _notificationChannel;
Function(Map<String, dynamic>)? onNotificationTap;
Future<void> init() async {
const androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
await _localNotifications.initialize(
const InitializationSettings(
android: androidSettings,
iOS: iosSettings,
),
onDidReceiveNotificationResponse: _onNotificationTap,
);
if (Platform.isAndroid) {
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(const AndroidNotificationChannel(
'el3ab_game',
'Game Notifications',
description: 'Match invites, turn alerts, tournament updates',
importance: Importance.high,
));
}
}
void subscribeToNotifications(String userId) {
final supabase = Supabase.instance.client;
_notificationChannel?.unsubscribe();
_notificationChannel = supabase
.channel('notifications:$userId')
.onPostgresChanges(
event: PostgresChangeEvent.insert,
schema: 'public',
table: 'notifications',
filter: PostgresChangeFilter(
type: PostgresChangeFilterType.eq,
column: 'user_id',
value: userId,
),
callback: (payload) {
final record = payload.newRecord;
_showLocalNotification(record);
},
)
.subscribe();
}
void unsubscribe() {
_notificationChannel?.unsubscribe();
_notificationChannel = null;
}
Future<void> registerToken(String token, String platform) async {
final supabase = Supabase.instance.client;
final userId = supabase.auth.currentUser?.id;
if (userId == null) return;
await supabase.from('push_tokens').upsert({
'user_id': userId,
'token': token,
'platform': platform,
'updated_at': DateTime.now().toIso8601String(),
}, onConflict: 'user_id,platform');
}
void _showLocalNotification(Map<String, dynamic> record) {
final title = record['title_ar'] ?? record['title'] ?? 'EL3AB';
final body = record['body_ar'] ?? record['body'] ?? '';
final data = record['data'] as Map<String, dynamic>? ?? {};
_localNotifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
NotificationDetails(
android: const AndroidNotificationDetails(
'el3ab_game',
'Game Notifications',
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(data),
);
}
void _onNotificationTap(NotificationResponse response) {
if (response.payload == null) return;
try {
final data = jsonDecode(response.payload!) as Map<String, dynamic>;
onNotificationTap?.call(data);
} catch (e) {
debugPrint('[Push] Error parsing notification payload: $e');
}
}
}
import 'package:share_plus/share_plus.dart';
class ShareService {
Future<void> share({
required String text,
String? subject,
}) async {
await Share.share(text, subject: subject);
}
Future<void> shareMatchInvite(String matchId, String gameType) async {
final url = 'https://el3ab-player.caprover.al-arcade.com/match/$matchId';
await share(
text: 'تعال نلعب $gameType على EL3AB! 🎮\n$url',
subject: 'دعوة لعب على EL3AB',
);
}
Future<void> shareProfile(String userId, String playerName) async {
final url = 'https://el3ab-player.caprover.al-arcade.com/profile/$userId';
await share(
text: 'شوف بروفايل $playerName على EL3AB 👑\n$url',
subject: 'بروفايل $playerName',
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../../config/theme.dart';
import '../../core/bridge/bridge_commands.dart';
import '../../core/bridge/js_bridge.dart';
import '../../core/services/auth_service.dart';
import '../../core/services/connectivity_service.dart';
import '../../core/services/deep_link_service.dart';
import '../../core/services/haptics_service.dart';
import '../../core/services/push_service.dart';
import '../../core/services/share_service.dart';
import 'webview_container.dart';
class AppShell extends StatefulWidget {
final AuthService authService;
final PushService pushService;
final DeepLinkService deepLinkService;
final ConnectivityService connectivityService;
final String? initialToken;
const AppShell({
super.key,
required this.authService,
required this.pushService,
required this.deepLinkService,
required this.connectivityService,
this.initialToken,
});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> with WidgetsBindingObserver {
final JsBridge _bridge = JsBridge();
final HapticsService _haptics = HapticsService();
final ShareService _share = ShareService();
bool _isGameMode = false;
bool _isOffline = false;
InAppWebViewController? _webController;
DateTime? _backgroundedAt;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_setupBridge();
_setupPushHandler();
_setupDeepLinks();
_setupConnectivity();
}
void _setupBridge() {
_bridge.on(BridgeCommand.haptic, (msg) {
_haptics.trigger(msg.data['type'] as String? ?? 'light');
});
_bridge.on(BridgeCommand.share, (msg) {
_share.share(
text: msg.data['text'] as String? ?? '',
subject: msg.data['title'] as String?,
);
});
_bridge.on(BridgeCommand.navSync, (msg) {
final gameMode = msg.data['gameMode'] as bool? ?? false;
if (gameMode != _isGameMode) {
setState(() => _isGameMode = gameMode);
SystemChrome.setEnabledSystemUIMode(
gameMode ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge,
);
}
});
_bridge.on(BridgeCommand.authReady, (msg) async {
final token = msg.data['token'] as String?;
final userId = msg.data['userId'] as String?;
if (token != null) {
await widget.authService.saveToken(token);
if (userId != null) {
await widget.authService.saveUserId(userId);
}
// Register push token with backend
_registerPushToken(userId);
// Offer biometric setup if available and not already enabled
_offerBiometric();
}
});
_bridge.on(BridgeCommand.authLogout, (msg) async {
await widget.authService.clearCredentials();
});
_bridge.on(BridgeCommand.badge, (msg) {});
}
void _setupPushHandler() {
widget.pushService.onNotificationTap = (data) {
_bridge.onPushTap(data);
};
}
void _setupDeepLinks() {
widget.deepLinkService.links.listen((link) {
_bridge.onDeepLink(link.route, link.id ?? '');
});
}
void _setupConnectivity() {
_isOffline = !widget.connectivityService.isOnline;
widget.connectivityService.onConnectivityChanged.listen((online) {
setState(() => _isOffline = !online);
_bridge.onConnectivityChanged(online);
});
}
Future<void> _registerPushToken(String? userId) async {
if (userId == null) return;
widget.pushService.subscribeToNotifications(userId);
}
Future<void> _offerBiometric() async {
final available = await widget.authService.isBiometricAvailable;
final alreadyEnabled = await widget.authService.isBiometricEnabled;
if (available && !alreadyEnabled && mounted) {
// Show biometric opt-in after a short delay
Future.delayed(const Duration(seconds: 3), () {
if (!mounted) return;
_showBiometricDialog();
});
}
}
void _showBiometricDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: El3abColors.bgCard,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text(
'تسجيل دخول سريع',
style: TextStyle(color: El3abColors.textPrimary),
textAlign: TextAlign.center,
),
content: const Text(
'فعّل البصمة لتسجيل الدخول بسرعة في المرة الجاية',
style: TextStyle(color: El3abColors.textSecondary),
textAlign: TextAlign.center,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('لاحقاً', style: TextStyle(color: El3abColors.textSecondary)),
),
FilledButton(
onPressed: () async {
await widget.authService.setBiometricEnabled(true);
if (ctx.mounted) Navigator.pop(ctx);
},
style: FilledButton.styleFrom(
backgroundColor: El3abColors.gold,
foregroundColor: Colors.black,
),
child: const Text('فعّل'),
),
],
),
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_backgroundedAt = DateTime.now();
} else if (state == AppLifecycleState.resumed) {
if (_backgroundedAt != null) {
final elapsed = DateTime.now().difference(_backgroundedAt!);
if (elapsed > const Duration(minutes: 5)) {
// Reload WebView after long background
_webController?.reload();
}
}
_backgroundedAt = null;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_bridge.detach();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: El3abColors.bgDeep,
body: SafeArea(
bottom: false,
child: WebViewContainer(
bridge: _bridge,
initialToken: widget.initialToken,
isOffline: _isOffline,
onControllerReady: (controller) {
_webController = controller;
},
),
),
);
}
}
import 'package:flutter/material.dart';
import '../../config/theme.dart';
class El3abBottomNav extends StatelessWidget {
final int currentIndex;
final ValueChanged<int> onTap;
final int badgeCount;
final bool isGameMode;
const El3abBottomNav({
super.key,
required this.currentIndex,
required this.onTap,
this.badgeCount = 0,
this.isGameMode = false,
});
static const List<_NavItem> _items = [
_NavItem(icon: Icons.emoji_events_rounded, label: 'الترتيب', world: 'rank'),
_NavItem(icon: Icons.people_rounded, label: 'اجتماعي', world: 'social'),
_NavItem(icon: Icons.sports_esports_rounded, label: 'العب', world: 'play'),
_NavItem(icon: Icons.military_tech_rounded, label: 'البطولات', world: 'tournaments'),
_NavItem(icon: Icons.person_rounded, label: 'حسابي', world: 'profile'),
];
static String worldForIndex(int index) => _items[index].world;
static int indexForWorld(String world) {
final idx = _items.indexWhere((item) => item.world == world);
return idx >= 0 ? idx : 2;
}
@override
Widget build(BuildContext context) {
if (isGameMode) return const SizedBox.shrink();
return Container(
decoration: const BoxDecoration(
color: El3abColors.bgBase,
border: Border(
top: BorderSide(color: El3abColors.border, width: 1),
),
),
child: SafeArea(
top: false,
child: SizedBox(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(_items.length, (index) {
final item = _items[index];
final isActive = index == currentIndex;
final isCenter = index == 2;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onTap(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(6),
decoration: isActive
? BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: El3abColors.gold.withValues(alpha: 0.12),
)
: null,
child: Icon(
item.icon,
size: isCenter ? 28 : 24,
color: isActive
? El3abColors.gold
: El3abColors.textSecondary,
),
),
if (index == 1 && badgeCount > 0)
Positioned(
right: -2,
top: -2,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: El3abColors.error,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
badgeCount > 9 ? '9+' : '$badgeCount',
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
),
],
),
const SizedBox(height: 2),
Text(
item.label,
style: TextStyle(
fontSize: 10,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive
? El3abColors.gold
: El3abColors.textSecondary,
),
),
],
),
),
);
}),
),
),
),
);
}
}
class _NavItem {
final IconData icon;
final String label;
final String world;
const _NavItem({
required this.icon,
required this.label,
required this.world,
});
}
import 'package:flutter/material.dart';
import '../../config/theme.dart';
class OfflineScreen extends StatefulWidget {
final VoidCallback onRetry;
const OfflineScreen({super.key, required this.onRetry});
@override
State<OfflineScreen> createState() => _OfflineScreenState();
}
class _OfflineScreenState extends State<OfflineScreen>
with SingleTickerProviderStateMixin {
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
bool _retrying = false;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
)..repeat(reverse: true);
_pulseAnimation = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
void _handleRetry() {
setState(() => _retrying = true);
widget.onRetry();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) setState(() => _retrying = false);
});
}
@override
Widget build(BuildContext context) {
return Container(
color: El3abColors.bgDeep,
child: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Animated wifi-off icon
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Opacity(
opacity: _pulseAnimation.value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: El3abColors.bgCard,
border: Border.all(
color: El3abColors.gold.withValues(alpha: 0.3),
width: 2,
),
),
child: const Icon(
Icons.wifi_off_rounded,
size: 48,
color: El3abColors.gold,
),
),
);
},
),
const SizedBox(height: 32),
const Text(
'لا يوجد اتصال',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: El3abColors.textPrimary,
),
),
const SizedBox(height: 12),
const Text(
'تأكد من اتصالك بالإنترنت وحاول مرة ثانية',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: El3abColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: 40),
// Retry button
SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
onPressed: _retrying ? null : _handleRetry,
style: FilledButton.styleFrom(
backgroundColor: El3abColors.gold,
foregroundColor: Colors.black,
disabledBackgroundColor: El3abColors.bgElevated,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: _retrying
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(
El3abColors.textSecondary),
),
)
: const Text(
'إعادة المحاولة',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),
),
);
}
}
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../../config/constants.dart';
import '../../config/theme.dart';
import '../../core/bridge/js_bridge.dart';
import 'offline_screen.dart';
class WebViewContainer extends StatefulWidget {
final JsBridge bridge;
final String? initialToken;
final Function(InAppWebViewController) onControllerReady;
final VoidCallback? onPageLoaded;
final bool isOffline;
const WebViewContainer({
super.key,
required this.bridge,
required this.onControllerReady,
this.initialToken,
this.onPageLoaded,
this.isOffline = false,
});
@override
State<WebViewContainer> createState() => WebViewContainerState();
}
class WebViewContainerState extends State<WebViewContainer> {
InAppWebViewController? _controller;
bool _isLoading = true;
bool _hasLoadError = false;
double _progress = 0;
bool _pageLoadedOnce = false;
@override
void didUpdateWidget(WebViewContainer oldWidget) {
super.didUpdateWidget(oldWidget);
// When connectivity comes back, auto-retry if we had an error
if (oldWidget.isOffline && !widget.isOffline && _hasLoadError) {
_retry();
}
}
void _retry() {
setState(() {
_hasLoadError = false;
_isLoading = true;
});
if (_pageLoadedOnce) {
_controller?.reload();
} else {
_controller?.loadUrl(
urlRequest: URLRequest(url: WebUri(AppConstants.webPlayerUrl)),
);
}
}
bool get _shouldShowOffline => _hasLoadError || (widget.isOffline && !_pageLoadedOnce);
@override
Widget build(BuildContext context) {
return Stack(
children: [
// WebView — always mounted so it keeps state, but hidden behind offline screen
Opacity(
opacity: _shouldShowOffline ? 0 : 1,
child: IgnorePointer(
ignoring: _shouldShowOffline,
child: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(AppConstants.webPlayerUrl),
),
initialUserScripts: UnmodifiableListView([
UserScript(
source: '''
window.IS_NATIVE_APP = true;
document.documentElement.classList.add('native-app');
''',
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
),
]),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
domStorageEnabled: true,
databaseEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
transparentBackground: true,
userAgent:
'El3abApp/1.0 (Flutter; ${Theme.of(context).platform == TargetPlatform.iOS ? "iOS" : "Android"})',
allowFileAccessFromFileURLs: false,
allowUniversalAccessFromFileURLs: false,
useShouldOverrideUrlLoading: true,
supportZoom: false,
disableHorizontalScroll: false,
disableVerticalScroll: false,
javaScriptCanOpenWindowsAutomatically: true,
supportMultipleWindows: true,
),
onWebViewCreated: (controller) {
_controller = controller;
widget.bridge.attach(controller);
widget.onControllerReady(controller);
controller.addJavaScriptHandler(
handlerName: 'flutter_bridge',
callback: (args) {
if (args.isNotEmpty) {
widget.bridge.handleMessage(args[0].toString());
}
return null;
},
);
},
onLoadStart: (controller, url) {
setState(() {
_isLoading = true;
_hasLoadError = false;
});
},
onLoadStop: (controller, url) async {
setState(() {
_isLoading = false;
_pageLoadedOnce = true;
});
await _injectBridgeScript();
if (widget.initialToken != null) {
await _injectToken(widget.initialToken!);
}
widget.onPageLoaded?.call();
},
onProgressChanged: (controller, progress) {
setState(() => _progress = progress / 100.0);
},
onReceivedError: (controller, request, error) {
// Catch all WebView errors (no internet, DNS failure, timeout, etc.)
final isMainFrame =
request.url.toString() == AppConstants.webPlayerUrl ||
request.isForMainFrame == true;
if (isMainFrame) {
setState(() {
_hasLoadError = true;
_isLoading = false;
});
}
},
onReceivedHttpError: (controller, request, response) {
// Catch server errors (500, 502, 503, etc.)
if (response.statusCode != null && response.statusCode! >= 500) {
final isMainFrame =
request.url.toString() == AppConstants.webPlayerUrl ||
request.isForMainFrame == true;
if (isMainFrame) {
setState(() {
_hasLoadError = true;
_isLoading = false;
});
}
}
},
onCreateWindow: (controller, createWindowAction) async {
// Handle OAuth popups (Google Sign-In etc.) — load in same WebView
final url = createWindowAction.request.url;
if (url != null) {
controller.loadUrl(urlRequest: URLRequest(url: url));
}
return true;
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
final uri = navigationAction.request.url;
if (uri == null) return NavigationActionPolicy.CANCEL;
final host = uri.host;
final isAllowed = AppConstants.allowedDomains.any(
(domain) => host == domain || host.endsWith('.$domain'),
);
if (isAllowed) {
return NavigationActionPolicy.ALLOW;
}
return NavigationActionPolicy.CANCEL;
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('[WebView] ${consoleMessage.message}');
},
),
),
),
// Native offline screen — covers WebView when there's an error
if (_shouldShowOffline)
Positioned.fill(
child: OfflineScreen(onRetry: _retry),
),
// Loading bar (only shows when actively loading, NOT when offline)
if (_isLoading && !_shouldShowOffline)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(
value: _progress > 0 ? _progress : null,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(El3abColors.gold),
minHeight: 2,
),
),
],
);
}
Future<void> _injectBridgeScript() async {
if (_controller == null) return;
const bridgeScript = '''
(function() {
if (window._el3ab_bridge_injected) return;
window._el3ab_bridge_injected = true;
window.flutter_bridge = {
postMessage: function(message) {
window.flutter_inappwebview.callHandler('flutter_bridge', message);
}
};
window.el3ab_native = window.el3ab_native || {};
window.IS_NATIVE_APP = true;
document.documentElement.classList.add('native-app');
console.log('[El3ab Native] Bridge injected');
})();
''';
await _controller!.evaluateJavascript(source: bridgeScript);
}
Future<void> _injectToken(String token) async {
if (_controller == null) return;
await _controller!.evaluateJavascript(
source: '''
(function() {
var state = JSON.parse(localStorage.getItem('el3ab_state') || '{}');
if (state.auth) {
state.auth.token = '$token';
}
localStorage.setItem('el3ab_state', JSON.stringify(state));
if (window.el3ab_native && window.el3ab_native.onTokenRefreshed) {
window.el3ab_native.onTokenRefreshed({token: '$token'});
}
})();
''',
);
}
Future<void> reload() async {
_retry();
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../config/theme.dart';
import '../../core/services/auth_service.dart';
class SplashScreen extends StatefulWidget {
final AuthService authService;
final VoidCallback onComplete;
final VoidCallback onBiometricSuccess;
const SplashScreen({
super.key,
required this.authService,
required this.onComplete,
required this.onBiometricSuccess,
});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
late Animation<double> _glowAnimation;
@override
void initState() {
super.initState();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
));
_controller = AnimationController(
duration: const Duration(milliseconds: 1800),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.6, curve: Curves.elasticOut),
),
);
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
),
);
_glowAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.4, 1.0, curve: Curves.easeInOut),
),
);
_controller.forward();
_initializeApp();
}
Future<void> _initializeApp() async {
await Future.delayed(const Duration(milliseconds: 1500));
final hasCredentials = await widget.authService.hasStoredCredentials();
final biometricEnabled = await widget.authService.isBiometricEnabled;
if (hasCredentials && biometricEnabled) {
final authenticated = await widget.authService.authenticateWithBiometric();
if (authenticated) {
widget.onBiometricSuccess();
return;
}
}
widget.onComplete();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: El3abColors.bgDeep,
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo with glow effect
Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: El3abColors.gold
.withValues(alpha: 0.4 * _glowAnimation.value),
blurRadius: 40 * _glowAnimation.value,
spreadRadius: 5 * _glowAnimation.value,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.cover,
),
),
),
),
),
const SizedBox(height: 24),
// Brand name
Opacity(
opacity: _opacityAnimation.value,
child: const Text(
'EL3AB',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w800,
color: El3abColors.gold,
letterSpacing: 4,
),
),
),
const SizedBox(height: 8),
Opacity(
opacity: _glowAnimation.value,
child: const Text(
'العب • نافس • فوز',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: El3abColors.textSecondary,
),
),
),
const SizedBox(height: 48),
// Loading indicator
Opacity(
opacity: _glowAnimation.value,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
El3abColors.gold.withValues(alpha: 0.6),
),
),
),
),
],
),
);
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'app.dart';
import 'config/constants.dart';
import 'core/services/auth_service.dart';
import 'core/services/connectivity_service.dart';
import 'core/services/deep_link_service.dart';
import 'core/services/push_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Color(0xFF0A1020),
systemNavigationBarIconBrightness: Brightness.light,
));
await Supabase.initialize(
url: AppConstants.supabaseUrl,
publishableKey: AppConstants.supabaseAnonKey,
);
final authService = AuthService();
final pushService = PushService();
final deepLinkService = DeepLinkService();
final connectivityService = ConnectivityService();
await Future.wait([
pushService.init(),
deepLinkService.init(),
connectivityService.init(),
]);
runApp(El3abApp(
authService: authService,
pushService: pushService,
deepLinkService: deepLinkService,
connectivityService: connectivityService,
));
}
This diff is collapsed.
name: el3ab
description: "EL3AB Social Gaming Hub for Arabic Speakers"
publish_to: 'none'
version: 3.0.1+10
environment:
sdk: ^3.12.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# WebView
flutter_inappwebview: ^6.1.5
# Supabase
supabase_flutter: ^2.8.1
# Local Notifications
flutter_local_notifications: ^18.0.1
# Storage
flutter_secure_storage: ^9.2.4
hive_flutter: ^1.1.0
shared_preferences: ^2.3.4
# Auth
local_auth: ^2.3.0
# Native Features
share_plus: ^10.1.4
url_launcher: ^6.3.1
connectivity_plus: ^6.1.2
permission_handler: ^11.3.1
app_links: ^6.3.3
# UI
cached_network_image: ^3.4.1
shimmer: ^3.0.0
# Utils
path_provider: ^2.1.5
package_info_plus: ^8.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment