Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
a6797435
Commit
a6797435
authored
Nov 05, 2025
by
DatHV
Browse files
refactor print, log, request
parent
f0334970
Changes
117
Expand all
Hide whitespace changes
Inline
Side-by-side
add_url_params.sh
deleted
100755 → 0
View file @
f0334970
#!/bin/bash
# Script để tự động thêm URL parameter handling vào index.html sau khi build
echo
"🔧 Adding URL parameter handling to index.html..."
# Tìm file index.html trong build/web
INDEX_FILE
=
"build/web/index.html"
if
[
!
-f
"
$INDEX_FILE
"
]
;
then
echo
"❌ File
$INDEX_FILE
not found!"
exit
1
fi
# Tạo backup
cp
"
$INDEX_FILE
"
"
$INDEX_FILE
.backup"
# Không cần thêm JavaScript nữa vì Flutter đọc trực tiếp từ URL
echo
"✅ No JavaScript needed - Flutter reads directly from URL query parameters"
echo
"✅ Added URL parameter handling to
$INDEX_FILE
"
echo
"🎉 Done! Now you can test with URLs like:"
echo
" http://localhost:8080/?token=abc123"
echo
" http://localhost:8080/?token=abc123&userId=user456"
export_dev.sh
View file @
a6797435
#!/
bin/
bash
#!/
usr/bin/env
bash
# Script để export web app cho môi trường development và chạy với CORS
# Build and package the Flutter web app for the DEV environment with aggressive optimizations.
# Produces a timestamped folder + zip in the project root, with precompressed assets.
echo
"🔧 Exporting Development Web App..."
set
-euo
pipefail
# Kill server cũ
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
lsof
-i
:8080 |
awk
'NR>1 {print $2}'
| xargs
kill
-9
2>/dev/null
||
true
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
TIMESTAMP
=
"
$(
date
+%Y%m%d_%H%M%S
)
"
OUT_DIR
=
"web_dev_export_
${
TIMESTAMP
}
"
ZIP_FILE
=
"
${
OUT_DIR
}
.zip"
# Export web app cho môi trường dev
cd
"
${
PROJECT_ROOT
}
"
./export_web.sh dev
# Chạy server với CORS như run_dev
set_env_dev
()
{
echo
"🚀 Starting exported web app with CORS..."
echo
"🔧 Switching to DEVELOPMENT environment..."
EXPORT_DIRS
=
$(
ls
-d
web_export_
*
2>/dev/null |
grep
-v
"
\.
zip$"
|
sort
-r
|
head
-1
)
local
src
=
"assets/config/env_dev.json"
if
[
!
-f
"
${
src
}
"
]
;
then
if
[
-z
"
$EXPORT_DIRS
"
]
;
then
echo
"❌
${
src
}
not found"
>
&2
echo
"❌ No web export directory found"
exit
1
exit
1
fi
fi
cp
"
${
src
}
"
assets/config/env.json
echo
"📁 Using export directory:
$EXPORT_DIRS
"
echo
"📋 Current config:"
cd
"
$EXPORT_DIRS
"
cat
assets/config/env.json
}
# Verify we're in the right directory
if
[
!
-f
"index.html"
]
;
then
copy_x_app_sdk
()
{
echo
"❌ index.html not found in
$EXPORT_DIRS
"
local
src
=
"node_modules/x-app-sdk/dist/index.es.js"
local
dest
=
"build/web/js/x_app_sdk_bundle.js"
if
[
!
-f
"
${
src
}
"
]
;
then
echo
"❌ x-app-sdk bundle not found at
${
src
}
"
>
&2
exit
1
exit
1
fi
mkdir
-p
"
$(
dirname
"
${
dest
}
"
)
"
cp
"
${
src
}
"
"
${
dest
}
"
echo
"✅ Copied x-app-sdk bundle."
}
compress_assets
()
{
local dir
=
"
$1
"
if
command
-v
gzip
>
/dev/null 2>&1
;
then
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.css'
-o
-name
'*.json'
-o
-name
'*.wasm'
-o
-name
'*.svg'
-o
-name
'*.html'
\)
-exec
gzip
-9
-kf
{}
\;
else
echo
"⚠️ gzip not available, skipping .gz artifacts"
fi
if
command
-v
brotli
>
/dev/null 2>&1
;
then
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.css'
-o
-name
'*.json'
-o
-name
'*.wasm'
-o
-name
'*.svg'
-o
-name
'*.html'
\)
-exec
brotli
-f
-k
-q
11
{}
\;
else
echo
"⚠️ brotli not available, skipping .br artifacts"
fi
}
echo
"🚀 Building DEV export (optimized)..."
set_env_dev
echo
"🧹 Cleaning previous artifacts..."
flutter clean
||
true
rm
-rf
.dart_tool build
||
true
echo
"📦 Getting Flutter packages..."
flutter pub get
echo
"🔨 Flutter build web (release, CanvasKit, no source maps)..."
flutter build web
\
--release
\
--no-source-maps
\
--pwa-strategy
=
none
\
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
true
\
--dart-define
=
FLUTTER_WEB_USE_SKWASM
=
false
\
--no-wasm-dry-run
copy_x_app_sdk
echo
"📁 Preparing export directory:
${
OUT_DIR
}
"
mkdir
-p
"
${
OUT_DIR
}
"
cp
-r
build/web/
*
"
${
OUT_DIR
}
/"
if
[
-f
"web/firebase-messaging-sw.js"
]
&&
[
!
-f
"
${
OUT_DIR
}
/firebase-messaging-sw.js"
]
;
then
cp
web/firebase-messaging-sw.js
"
${
OUT_DIR
}
/"
fi
fi
echo
"✅ Found index.html in export directory"
# Helper script for local preview with SPA fallback
cat
>
"
${
OUT_DIR
}
/serve_local.sh"
<<
'
EOF
'
#!/usr/bin/env bash
# Start web server with CORS headers (same as run_web_complete.sh)
PORT="
${
1
:-
8080
}
"
python3
-c
"
python3 - "
$PORT
" <<'PY'
import http.server
import http.server
import socketserver
import socketserver
import socket
import os
import os
import sys
ROOT = os.getcwd()
PORT = int(sys.argv[1])
class SPAHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
resolved = self.translate_path(self.path)
if not os.path.exists(resolved):
self.path = '/index.html'
return super().do_GET()
class CORSHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
def end_headers(self):
print(f"🌐 {fmt % args}")
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
with socketserver.TCPServer(('', PORT), SPAHandler) as httpd:
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin')
print(f"🚀 Serving {ROOT} at http://localhost:{PORT}")
super().end_headers()
print("💡 SPA fallback enabled. Ctrl+C to stop.")
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
print(f'🌐 {format % args}')
def find_free_port(start_port=8080, max_attempts=10):
for port in range(start_port, start_port + max_attempts):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', port))
return port
except OSError:
continue
return None
PORT = find_free_port(8080, 20)
if not PORT:
print('❌ No free port found')
exit(1)
print(f'🚀 Server running at http://localhost:{PORT}')
print(f'📁 Serving from: {os.getcwd()}')
print('🔧 CORS headers enabled for API calls')
print('')
print('Press Ctrl+C to stop the server')
with socketserver.TCPServer(('', PORT), CORSHTTPRequestHandler) as httpd:
httpd.serve_forever()
httpd.serve_forever()
"
&
PY
SERVER_PID
=
$!
EOF
chmod
+x
"
${
OUT_DIR
}
/serve_local.sh"
# Wait for server to start
sleep
3
if
[
"
${
KEEP_CANVASKIT
:-
0
}
"
!=
"1"
]
;
then
echo
"🧹 Removing CanvasKit bundle to shrink export (set KEEP_CANVASKIT=1 to keep)..."
# Open browser with CORS disabled (same as run_web_complete.sh)
rm
-rf
"
${
OUT_DIR
}
/canvaskit"
echo
"🌐 Opening browser with CORS disabled..."
if
command
-v
open &> /dev/null
;
then
# macOS
open
-n
-a
"Google Chrome"
--args
--disable-web-security
--user-data-dir
=
/tmp/chrome_dev
--disable-features
=
VizDisplayCompositor http://localhost:8080
elif
command
-v
google-chrome &> /dev/null
;
then
# Linux
google-chrome
--disable-web-security
--user-data-dir
=
/tmp/chrome_dev
--disable-features
=
VizDisplayCompositor http://localhost:8080 &
else
else
echo
"
⚠
️
Chrome not found. Please open manually: http://localhost:8080
"
echo
"
ℹ
️
KEEP_CANVASKIT=1 → giữ nguyên thư mục canvaskit.
"
fi
fi
echo
"🗜️ Precompressing assets..."
compress_assets
"
${
OUT_DIR
}
"
echo
"📦 Creating zip archive
${
ZIP_FILE
}
..."
zip
-rq
"
${
ZIP_FILE
}
"
"
${
OUT_DIR
}
"
echo
""
echo
""
echo
"✅ Setup complete!"
echo
"🎉 DEV export ready!"
echo
"🌐 Web app: http://localhost:8080"
echo
" Folder :
${
OUT_DIR
}
"
echo
"🔧 CORS disabled in browser for development"
echo
" Zip :
${
ZIP_FILE
}
"
echo
"📁 Export directory:
$EXPORT_DIRS
"
echo
""
echo
""
echo
"Press Ctrl+C to stop the server"
echo
"📌 Upload suggestion: serve files with Content-Encoding gzip/brotli where available."
echo
""
# Wait for user to stop
echo
"▶️ Quick preview command:"
wait
$SERVER_PID
echo
" cd
${
OUT_DIR
}
&& ./serve_local.sh 8080"
echo
"⚠️ Avoid using 'python3 -m http.server' directly; it does not handle SPA routes and will 404 on deep links."
export_prod.sh
0 → 100755
View file @
a6797435
#!/usr/bin/env bash
# Build and package the Flutter web app for PRODUCTION delivery with optimizations.
set
-euo
pipefail
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
TIMESTAMP
=
"
$(
date
+%Y%m%d_%H%M%S
)
"
OUT_DIR
=
"web_prod_export_
${
TIMESTAMP
}
"
ZIP_FILE
=
"
${
OUT_DIR
}
.zip"
cd
"
${
PROJECT_ROOT
}
"
write_prod_env
()
{
cat
>
assets/config/env.json
<<
'
EOF
'
{
"flavor":"pro",
"baseUrl":"https://api.mypoint.com.vn/8854/gup2start/rest",
"t3Token":"runner-env-flavor-pro",
"enableLogging":false
}
EOF
echo
"📋 Production config written to assets/config/env.json"
}
copy_x_app_sdk
()
{
local
src
=
"node_modules/x-app-sdk/dist/index.es.js"
local
dest
=
"build/web/js/x_app_sdk_bundle.js"
if
[
!
-f
"
${
src
}
"
]
;
then
echo
"❌ x-app-sdk bundle not found at
${
src
}
"
>
&2
exit
1
fi
mkdir
-p
"
$(
dirname
"
${
dest
}
"
)
"
cp
"
${
src
}
"
"
${
dest
}
"
echo
"✅ Copied x-app-sdk bundle."
}
compress_assets
()
{
local dir
=
"
$1
"
if
command
-v
gzip
>
/dev/null 2>&1
;
then
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.css'
-o
-name
'*.json'
-o
-name
'*.wasm'
-o
-name
'*.svg'
-o
-name
'*.html'
\)
-exec
gzip
-9
-kf
{}
\;
else
echo
"⚠️ gzip not available, skipping .gz artifacts"
fi
if
command
-v
brotli
>
/dev/null 2>&1
;
then
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.css'
-o
-name
'*.json'
-o
-name
'*.wasm'
-o
-name
'*.svg'
-o
-name
'*.html'
\)
-exec
brotli
-f
-k
-q
11
{}
\;
else
echo
"⚠️ brotli not available, skipping .br artifacts"
fi
}
echo
"🚀 Building PRODUCTION export (optimized)..."
write_prod_env
echo
"🧹 Cleaning previous artifacts..."
flutter clean
||
true
rm
-rf
.dart_tool build
||
true
echo
"📦 Getting Flutter packages..."
flutter pub get
echo
"🔨 Flutter build web (release, CanvasKit, no source maps)..."
flutter build web
\
--release
\
--no-source-maps
\
--pwa-strategy
=
none
\
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
true
\
--dart-define
=
FLUTTER_WEB_USE_SKWASM
=
false
\
--no-wasm-dry-run
copy_x_app_sdk
echo
"📁 Preparing export directory:
${
OUT_DIR
}
"
mkdir
-p
"
${
OUT_DIR
}
"
cp
-r
build/web/
*
"
${
OUT_DIR
}
/"
if
[
-f
"web/firebase-messaging-sw.js"
]
&&
[
!
-f
"
${
OUT_DIR
}
/firebase-messaging-sw.js"
]
;
then
cp
web/firebase-messaging-sw.js
"
${
OUT_DIR
}
/"
fi
# Helper script for local preview with SPA fallback
cat
>
"
${
OUT_DIR
}
/serve_local.sh"
<<
'
EOF
'
#!/usr/bin/env bash
PORT="
${
1
:-
8080
}
"
python3 - "
$PORT
" <<'PY'
import http.server
import socketserver
import os
import sys
ROOT = os.getcwd()
PORT = int(sys.argv[1])
class SPAHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
resolved = self.translate_path(self.path)
if not os.path.exists(resolved):
self.path = '/index.html'
return super().do_GET()
def log_message(self, fmt, *args):
print(f"🌐 {fmt % args}")
with socketserver.TCPServer(('', PORT), SPAHandler) as httpd:
print(f"🚀 Serving {ROOT} at http://localhost:{PORT}")
print("💡 SPA fallback enabled. Ctrl+C to stop.")
httpd.serve_forever()
PY
EOF
chmod
+x
"
${
OUT_DIR
}
/serve_local.sh"
if
[
"
${
KEEP_CANVASKIT
:-
0
}
"
!=
"1"
]
;
then
echo
"🧹 Removing CanvasKit bundle to shrink export (set KEEP_CANVASKIT=1 to keep)..."
rm
-rf
"
${
OUT_DIR
}
/canvaskit"
else
echo
"ℹ️ KEEP_CANVASKIT=1 → giữ nguyên thư mục canvaskit."
fi
echo
"🗜️ Precompressing assets..."
compress_assets
"
${
OUT_DIR
}
"
echo
"📦 Creating zip archive
${
ZIP_FILE
}
..."
zip
-rq
"
${
ZIP_FILE
}
"
"
${
OUT_DIR
}
"
echo
""
echo
"🎉 PRODUCTION export ready!"
echo
" Folder :
${
OUT_DIR
}
"
echo
" Zip :
${
ZIP_FILE
}
"
echo
""
echo
"📌 Deliverable: send the zip (or folder) to the hosting team. Serve with gzip/brotli if possible."
echo
""
echo
"▶️ Quick preview command:"
echo
" cd
${
OUT_DIR
}
&& ./serve_local.sh 8080"
echo
"⚠️ Avoid using 'python3 -m http.server' directly; it does not handle SPA routes and will 404 on deep links."
export_web.sh
deleted
100755 → 0
View file @
f0334970
#!/bin/bash
# Script để export Flutter web app thành HTML/CSS cho deployment
# Có thể chỉ định môi trường (mặc định: prod). Ví dụ: ./export_web.sh dev
TARGET_ENV
=
"
${
1
:-
prod
}
"
ENV_LABEL
=
$(
echo
"
$TARGET_ENV
"
|
tr
'[:lower:]'
'[:upper:]'
)
echo
"🚀 Exporting Flutter web app for
${
ENV_LABEL
}
environment..."
# Kill server cũ trên port 8080 (nếu có)
echo
"🛑 Stopping any existing server on :8080..."
lsof
-i
:8080 |
awk
'NR>1 {print $2}'
| xargs
kill
-9
2>/dev/null
||
true
# Switch to target environment automatically
echo
"🔧 Switching to
${
ENV_LABEL
}
environment..."
./scripts/switch_env.sh
"
$TARGET_ENV
"
# Clear cache build để tránh dính SW/cache cũ
echo
"🧹 Clearing build caches..."
flutter clean
rm
-rf
.dart_tool build
flutter pub get
# Install web dependencies
echo
"📦 Installing web dependencies..."
cd
web
if
[
-f
"package.json"
]
;
then
npm
install
if
[
$?
-eq
0
]
;
then
echo
"✅ Web dependencies installed successfully!"
else
echo
"⚠️ Warning: Failed to install web dependencies, continuing anyway..."
fi
else
echo
"⚠️ No package.json found in web directory, skipping npm install"
fi
cd
..
# Tạo thư mục export
EXPORT_DIR
=
"web_export_
${
TARGET_ENV
}
_
$(
date
+%Y%m%d_%H%M%S
)
"
echo
"📁 Creating export directory:
$EXPORT_DIR
"
mkdir
-p
"
$EXPORT_DIR
"
# Build web app (prefer HTML renderer). If your Flutter doesn't support --web-renderer, the dart-define will keep HTML path.
echo
"🔨 Building Flutter web app (HTML renderer preferred)..."
flutter build web
--release
--source-maps
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
false
if
[
$?
-eq
0
]
;
then
echo
"✅ Build thành công!"
echo
"📥 Copying x-app-sdk bundle into build artifacts..."
./scripts/copy_x_app_sdk_bundle.sh
# Copy toàn bộ nội dung từ build/web
echo
"📦 Copying web files..."
cp
-r
build/web/
*
"
$EXPORT_DIR
/"
# Copy firebase-messaging-sw.js nếu chưa có
if
[
!
-f
"
$EXPORT_DIR
/firebase-messaging-sw.js"
]
;
then
cp
web/firebase-messaging-sw.js
"
$EXPORT_DIR
/"
fi
# Tạo file test để demo URL parameters
cat
>
"
$EXPORT_DIR
/test_urls.html"
<<
'
EOF
'
<!DOCTYPE html>
<html>
<head>
<title>Test URL Parameters</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-link { display: block; margin: 10px 0; padding: 10px; background: #f0f0f0; text-decoration: none; color: #333; }
.test-link:hover { background: #e0e0e0; }
</style>
</head>
<body>
<h1>Test URL Parameters</h1>
<p>Click các link dưới đây để test URL parameters:</p>
<a href="?token=abc123" class="test-link">Test với token: abc123</a>
<a href="?token=abc123&userId=user456" class="test-link">Test với token + userId</a>
<a href="?token=admin789&userId=admin001" class="test-link">Test với admin token</a>
<a href="/" class="test-link">Test không có parameters (onboarding)</a>
<h2>Debug Info:</h2>
<div id="debug-info"></div>
<script>
// Hiển thị thông tin debug
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const userId = urlParams.get('userId');
document.getElementById('debug-info').innerHTML = `
<p><strong>Current URL:</strong>
${
window
.location.href
}
</p>
<p><strong>Token:</strong>
${
token
||
'None'
}
</p>
<p><strong>UserId:</strong>
${
userId
||
'None'
}
</p>
<p><strong>localStorage url_token:</strong>
${
localStorage
.getItem(
'url_token'
) ||
'None'
}
</p>
<p><strong>localStorage url_user_id:</strong>
${
localStorage
.getItem(
'url_user_id'
) ||
'None'
}
</p>
`;
</script>
</body>
</html>
EOF
echo
"📝 Created test_urls.html for testing"
# Tạo file README cho bên web
cat
>
"
$EXPORT_DIR
/README_DEPLOYMENT.md"
<<
'
EOF
'
# MyPoint Flutter Web App - Deployment Guide
## Cấu trúc thư mục
- `index.html` - File chính của ứng dụng
- `main.dart.js` - JavaScript code của Flutter app
- `flutter_bootstrap.js` - Flutter bootstrap script
- `canvaskit/` - Flutter rendering engine
- `assets/` - Images, fonts, và data files
- `firebase-messaging-sw.js` - Firebase service worker
## Yêu cầu server
- Web server hỗ trợ serving static files
- HTTPS được khuyến nghị cho PWA features
- CORS headers nếu cần thiết cho API calls
## Cấu hình server
1. Serve tất cả files từ thư mục này
2. Đảm bảo MIME types đúng:
- `.js` files: `application/javascript`
- `.wasm` files: `application/wasm`
- `.json` files: `application/json`
- `.png/.jpg` files: `image/*`
## Environment Variables
App sử dụng các biến môi trường từ `assets/config/env.json`
Có thể override bằng query parameters: `?env=production`
## Firebase Configuration
Firebase config được load từ `firebase_options.dart`
Đảm bảo Firebase project được cấu hình đúng cho domain này.
## CORS Configuration
App cần CORS headers để gọi API:
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Accept, Origin
```
## Troubleshooting
- Nếu có lỗi CORS, cấu hình server để allow origin của domain
- Nếu có lỗi Firebase, kiểm tra domain trong Firebase console
- Nếu có lỗi assets, đảm bảo đường dẫn relative đúng
EOF
# Tạo file .htaccess cho Apache
cat
>
"
$EXPORT_DIR
/.htaccess"
<<
'
EOF
'
# Apache configuration for Flutter web app
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/wasm
</IfModule>
# Set correct MIME types
AddType application/javascript .js
AddType application/wasm .wasm
AddType application/json .json
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# CORS headers for API calls
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Accept, Origin"
Header always set Access-Control-Allow-Credentials "true"
</IfModule>
EOF
# Tạo file nginx.conf cho Nginx
cat
>
"
$EXPORT_DIR
/nginx.conf"
<<
'
EOF
'
# Nginx configuration for Flutter web app
server {
listen 80;
server_name your-domain.com;
root /path/to/web_export;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/wasm;
# Cache static assets
location ~*
\.
(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|wasm)
$
{
expires 1y;
add_header Cache-Control "public, immutable";
}
# Handle Flutter routes
location / {
try_files
$uri
$uri
/ /index.html;
}
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# CORS headers for API calls
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Accept, Origin";
add_header Access-Control-Allow-Credentials "true";
}
EOF
# Tạo file package.json cho Node.js hosting
cat
>
"
$EXPORT_DIR
/package.json"
<<
'
EOF
'
{
"name": "mypoint-flutter-web",
"version": "1.0.0",
"description": "MyPoint Flutter Web Application",
"main": "index.html",
"scripts": {
"start": "npx serve -s . -l 3000",
"build": "echo 'Already built by Flutter'",
"serve": "npx serve -s . -l 3000"
},
"dependencies": {
"serve": "^14.0.0"
},
"engines": {
"node": ">=14.0.0"
}
}
EOF
# Tạo file docker-compose.yml cho Docker deployment
cat
>
"
$EXPORT_DIR
/docker-compose.yml"
<<
'
EOF
'
version: '3.8'
services:
mypoint-web:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- .:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
EOF
# Tạo file Dockerfile
cat
>
"
$EXPORT_DIR
/Dockerfile"
<<
'
EOF
'
FROM nginx:alpine
# Copy web files
COPY . /usr/share/nginx/html/
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
EOF
# Tạo zip file để dễ dàng transfer
echo
"📦 Creating zip package..."
zip
-r
"
${
EXPORT_DIR
}
.zip"
"
$EXPORT_DIR
"
echo
""
echo
"✅ Export hoàn thành!"
echo
"📁 Thư mục export:
$EXPORT_DIR
"
echo
"📦 Zip file:
${
EXPORT_DIR
}
.zip"
echo
""
echo
"🚀 Cách deploy:"
echo
"1. Upload toàn bộ nội dung thư mục
$EXPORT_DIR
lên web server"
echo
"2. Cấu hình web server theo hướng dẫn trong README_DEPLOYMENT.md"
echo
"3. Hoặc sử dụng Docker: docker-compose up -d"
echo
"4. Hoặc sử dụng Node.js: npm install && npm start"
echo
""
echo
"🔧 X-App-SDK Integration:"
echo
""
echo
"🌐 Test local: cd
$EXPORT_DIR
&& python3 -m http.server 8080"
echo
"💡 Tip: Nếu muốn auto-run server, chạy: (cd
$EXPORT_DIR
&& python3 -m http.server 8080 &)"
else
echo
"❌ Build thất bại!"
exit
1
fi
lib/base/app_loading.dart
View file @
a6797435
import
'dart:async'
;
import
'dart:async'
;
import
'dart:collection'
;
import
'dart:collection'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'../configs/constants.dart'
;
import
'../configs/constants.dart'
;
...
@@ -64,7 +65,7 @@ class AppLoading {
...
@@ -64,7 +65,7 @@ class AppLoading {
double
size
=
56
,
double
size
=
56
,
double
strokeWidth
=
4
,
double
strokeWidth
=
4
,
})
{
})
{
p
rint
(
'AppLoading.show called'
);
debugP
rint
(
'AppLoading.show called'
);
// Đưa thao tác vào hàng đợi, không làm ngay
// Đưa thao tác vào hàng đợi, không làm ngay
_ops
.
add
(()
{
_ops
.
add
(()
{
if
(
isShowing
)
{
if
(
isShowing
)
{
...
@@ -109,7 +110,7 @@ class AppLoading {
...
@@ -109,7 +110,7 @@ class AppLoading {
}
}
void
hide
()
{
void
hide
()
{
p
rint
(
'AppLoading.hide called'
);
debugP
rint
(
'AppLoading.hide called'
);
_ops
.
add
(()
{
_ops
.
add
(()
{
_timer
?.
cancel
();
_timer
?.
cancel
();
_timer
=
null
;
_timer
=
null
;
...
...
lib/base/app_navigator.dart
View file @
a6797435
...
@@ -119,13 +119,11 @@ class AppNavigator {
...
@@ -119,13 +119,11 @@ class AppNavigator {
bool
showCloseButton
=
false
,
bool
showCloseButton
=
false
,
VoidCallback
?
onConfirmed
,
VoidCallback
?
onConfirmed
,
})
{
})
{
p
rint
(
"Show alert error:
$_errorDialogShown
"
);
debugP
rint
(
"Show alert error:
$_errorDialogShown
"
);
if
(
_errorDialogShown
)
return
;
if
(
_errorDialogShown
)
return
;
final
context
=
_ctx
??
Get
.
context
??
Get
.
overlayContext
;
final
context
=
_ctx
??
Get
.
context
??
Get
.
overlayContext
;
if
(
context
==
null
)
{
if
(
context
==
null
)
{
if
(
kDebugMode
)
{
debugPrint
(
'⚠️ AppNavigator: Unable to show alert, no context available'
);
print
(
'⚠️ AppNavigator: Unable to show alert, no context available'
);
}
return
;
return
;
}
}
_errorDialogShown
=
true
;
_errorDialogShown
=
true
;
...
...
lib/base/base_response_model.dart
View file @
a6797435
...
@@ -13,8 +13,8 @@ class BaseResponseModel<T> {
...
@@ -13,8 +13,8 @@ class BaseResponseModel<T> {
final
T
?
data
;
final
T
?
data
;
bool
get
isSuccess
{
bool
get
isSuccess
{
final
_c
ode
=
code
??
0
;
final
rawC
ode
=
code
??
0
;
if
(
_c
ode
>=
200
&&
_c
ode
<
299
)
return
true
;
if
(
rawC
ode
>=
200
&&
rawC
ode
<
299
)
return
true
;
return
status
?.
toUpperCase
()
==
"SUCCESS"
;
return
status
?.
toUpperCase
()
==
"SUCCESS"
;
}
}
...
...
lib/base/base_screen.dart
View file @
a6797435
...
@@ -65,13 +65,13 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -65,13 +65,13 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
/// Called when the widget is first inserted into the tree.
/// Called when the widget is first inserted into the tree.
/// Use this to initialize data, setup listeners, etc.
/// Use this to initialize data, setup listeners, etc.
void
onInit
()
{
void
onInit
()
{
if
(
kDebugMode
)
p
rint
(
"onInit:
$runtimeType
"
);
debugP
rint
(
"onInit:
$runtimeType
"
);
}
}
/// Called when the widget is removed from the tree.
/// Called when the widget is removed from the tree.
/// Use this to cleanup resources, cancel timers, etc.
/// Use this to cleanup resources, cancel timers, etc.
void
onDispose
()
{
void
onDispose
()
{
if
(
kDebugMode
)
p
rint
(
"onDispose:
$runtimeType
"
);
debugP
rint
(
"onDispose:
$runtimeType
"
);
}
}
// MARK: - Route Visibility Methods
// MARK: - Route Visibility Methods
...
@@ -79,25 +79,25 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -79,25 +79,25 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
/// Called when the route is about to become visible (push or uncovered).
/// Called when the route is about to become visible (push or uncovered).
/// Use this to prepare data, start animations, etc.
/// Use this to prepare data, start animations, etc.
void
onRouteWillAppear
()
{
void
onRouteWillAppear
()
{
if
(
kDebugMode
)
p
rint
(
"onRouteWillAppear:
$runtimeType
"
);
debugP
rint
(
"onRouteWillAppear:
$runtimeType
"
);
}
}
/// Called when the route has become visible.
/// Called when the route has become visible.
/// Use this to start timers, refresh data, etc.
/// Use this to start timers, refresh data, etc.
void
onRouteDidAppear
()
{
void
onRouteDidAppear
()
{
if
(
kDebugMode
)
p
rint
(
"onRouteDidAppear:
$runtimeType
"
);
debugP
rint
(
"onRouteDidAppear:
$runtimeType
"
);
}
}
/// Called when the route is about to be covered or popped.
/// Called when the route is about to be covered or popped.
/// Use this to pause operations, save state, etc.
/// Use this to pause operations, save state, etc.
void
onRouteWillDisappear
()
{
void
onRouteWillDisappear
()
{
if
(
kDebugMode
)
p
rint
(
"onRouteWillDisappear:
$runtimeType
"
);
debugP
rint
(
"onRouteWillDisappear:
$runtimeType
"
);
}
}
/// Called when the route has been covered or popped.
/// Called when the route has been covered or popped.
/// Use this to stop timers, cleanup temporary resources, etc.
/// Use this to stop timers, cleanup temporary resources, etc.
void
onRouteDidDisappear
()
{
void
onRouteDidDisappear
()
{
if
(
kDebugMode
)
p
rint
(
"onRouteDidDisappear:
$runtimeType
"
);
debugP
rint
(
"onRouteDidDisappear:
$runtimeType
"
);
}
}
// MARK: - App Lifecycle Methods
// MARK: - App Lifecycle Methods
...
@@ -105,13 +105,13 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -105,13 +105,13 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
/// Called when the app becomes active (foreground).
/// Called when the app becomes active (foreground).
/// Use this to resume operations, refresh data, etc.
/// Use this to resume operations, refresh data, etc.
void
onAppResumed
()
{
void
onAppResumed
()
{
if
(
kDebugMode
)
p
rint
(
"onAppResumed:
$runtimeType
"
);
debugP
rint
(
"onAppResumed:
$runtimeType
"
);
}
}
/// Called when the app becomes inactive (background).
/// Called when the app becomes inactive (background).
/// Use this to pause operations, save state, etc.
/// Use this to pause operations, save state, etc.
void
onAppPaused
()
{
void
onAppPaused
()
{
if
(
kDebugMode
)
p
rint
(
"onAppPaused:
$runtimeType
"
);
debugP
rint
(
"onAppPaused:
$runtimeType
"
);
}
}
// MARK: - UI Helper Methods
// MARK: - UI Helper Methods
...
...
lib/base/base_view_model.dart
View file @
a6797435
...
@@ -3,8 +3,6 @@ import 'package:fluttertoast/fluttertoast.dart';
...
@@ -3,8 +3,6 @@ import 'package:fluttertoast/fluttertoast.dart';
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/app_loading.dart'
;
import
'package:mypoint_flutter_app/base/app_loading.dart'
;
import
'../configs/constants.dart'
;
class
BaseViewModel
extends
GetxController
with
WidgetsBindingObserver
{
class
BaseViewModel
extends
GetxController
with
WidgetsBindingObserver
{
var
isShowLoading
=
false
;
var
isShowLoading
=
false
;
RxBool
isShowKey
=
false
.
obs
;
RxBool
isShowKey
=
false
.
obs
;
...
...
lib/configs/device_info.dart
View file @
a6797435
...
@@ -46,15 +46,15 @@ class DeviceInfo {
...
@@ -46,15 +46,15 @@ class DeviceInfo {
static
DeviceDetails
?
_cachedDetails
;
static
DeviceDetails
?
_cachedDetails
;
static
Future
<
String
>
getDeviceId
()
async
{
static
Future
<
String
>
getDeviceId
()
async
{
_cachedDeviceId
=
"13sd26fc-748f-4d1a-a064-af8d7874d565"
;
//
_cachedDeviceId = "13sd26fc-748f-4d1a-a064-af8d7874d565";
//
if (_cachedDeviceId != null) return _cachedDeviceId!;
if
(
_cachedDeviceId
!=
null
)
return
_cachedDeviceId
!;
//
final prefs = await SharedPreferences.getInstance();
final
prefs
=
await
SharedPreferences
.
getInstance
();
//
String? deviceId = prefs.getString(_deviceIdPreferenceKey);
String
?
deviceId
=
prefs
.
getString
(
_deviceIdPreferenceKey
);
//
if (deviceId == null || deviceId.isEmpty) {
if
(
deviceId
==
null
||
deviceId
.
isEmpty
)
{
//
deviceId = const Uuid().v4();
deviceId
=
const
Uuid
().
v4
();
//
await prefs.setString(_deviceIdPreferenceKey, deviceId);
await
prefs
.
setString
(
_deviceIdPreferenceKey
,
deviceId
);
//
}
}
//
_cachedDeviceId = deviceId;
_cachedDeviceId
=
deviceId
;
return
_cachedDeviceId
!;
return
_cachedDeviceId
!;
}
}
...
@@ -82,7 +82,7 @@ class DeviceInfo {
...
@@ -82,7 +82,7 @@ class DeviceInfo {
os
=
'Android'
;
os
=
'Android'
;
final
and
=
await
deviceInfo
.
androidInfo
;
final
and
=
await
deviceInfo
.
androidInfo
;
final
rel
=
and
.
version
.
release
;
final
rel
=
and
.
version
.
release
;
final
sdk
=
and
.
version
.
sdkInt
?
.
toString
()
??
''
;
final
sdk
=
and
.
version
.
sdkInt
.
toString
()
??
''
;
osVersion
=
sdk
.
isEmpty
?
rel
:
'
$rel
(SDK
$sdk
)'
;
osVersion
=
sdk
.
isEmpty
?
rel
:
'
$rel
(SDK
$sdk
)'
;
}
else
{
}
else
{
os
=
Platform
.
operatingSystem
;
os
=
Platform
.
operatingSystem
;
...
...
lib/core/app_initializer.dart
View file @
a6797435
...
@@ -17,7 +17,7 @@ import 'package:mypoint_flutter_app/core/deep_link_service.dart';
...
@@ -17,7 +17,7 @@ import 'package:mypoint_flutter_app/core/deep_link_service.dart';
class
AppInitializer
{
class
AppInitializer
{
/// Initialize all core app features
/// Initialize all core app features
static
Future
<
void
>
initialize
()
async
{
static
Future
<
void
>
initialize
()
async
{
p
rint
(
'🚀 Initializing app...'
);
debugP
rint
(
'🚀 Initializing app...'
);
// Load environment configuration
// Load environment configuration
await
loadEnv
();
await
loadEnv
();
// Initialize data preferences
// Initialize data preferences
...
@@ -34,43 +34,43 @@ class AppInitializer {
...
@@ -34,43 +34,43 @@ class AppInitializer {
await
_initializeWebFeatures
();
await
_initializeWebFeatures
();
// Initialize deep link handlers (Branch, URI schemes)
// Initialize deep link handlers (Branch, URI schemes)
await
DeepLinkService
().
initialize
();
await
DeepLinkService
().
initialize
();
p
rint
(
'✅ App initialization completed'
);
debugP
rint
(
'✅ App initialization completed'
);
}
}
/// Initialize web-specific features
/// Initialize web-specific features
static
Future
<
void
>
_initializeWebFeatures
()
async
{
static
Future
<
void
>
_initializeWebFeatures
()
async
{
if
(
kIsWeb
)
{
if
(
kIsWeb
)
{
p
rint
(
'🌐 Initializing web-specific features...'
);
debugP
rint
(
'🌐 Initializing web-specific features...'
);
try
{
try
{
// Initialize x-app-sdk
// Initialize x-app-sdk
await
webInitializeXAppSDK
();
await
webInitializeXAppSDK
();
await
_configureWebSdkHeader
();
await
_configureWebSdkHeader
();
p
rint
(
'✅ Web features initialized successfully'
);
debugP
rint
(
'✅ Web features initialized successfully'
);
}
catch
(
e
)
{
}
catch
(
e
)
{
p
rint
(
'❌ Error initializing web features:
$e
'
);
debugP
rint
(
'❌ Error initializing web features:
$e
'
);
}
}
}
else
{
}
else
{
p
rint
(
'📱 Skipping web features initialization for mobile'
);
debugP
rint
(
'📱 Skipping web features initialization for mobile'
);
}
}
}
}
/// Initialize Firebase and FCM (mobile only)
/// Initialize Firebase and FCM (mobile only)
static
Future
<
void
>
_initializeFirebase
()
async
{
static
Future
<
void
>
_initializeFirebase
()
async
{
if
(!
kIsWeb
)
{
if
(!
kIsWeb
)
{
p
rint
(
'📱 Initializing Firebase for mobile...'
);
debugP
rint
(
'📱 Initializing Firebase for mobile...'
);
await
initFirebaseAndFcm
();
await
initFirebaseAndFcm
();
}
else
{
}
else
{
p
rint
(
'🌐 Skipping Firebase initialization for web'
);
debugP
rint
(
'🌐 Skipping Firebase initialization for web'
);
}
}
}
}
/// Fetch user point if already logged in
/// Fetch user point if already logged in
static
Future
<
void
>
_fetchUserPointIfLoggedIn
()
async
{
static
Future
<
void
>
_fetchUserPointIfLoggedIn
()
async
{
if
(
DataPreference
.
instance
.
logged
)
{
if
(
DataPreference
.
instance
.
logged
)
{
p
rint
(
'💰 Fetching user point...'
);
debugP
rint
(
'💰 Fetching user point...'
);
await
UserPointManager
().
fetchUserPoint
();
await
UserPointManager
().
fetchUserPoint
();
}
else
{
}
else
{
p
rint
(
'👤 User not logged in, skipping point fetch'
);
debugP
rint
(
'👤 User not logged in, skipping point fetch'
);
}
}
}
}
...
@@ -80,18 +80,18 @@ class AppInitializer {
...
@@ -80,18 +80,18 @@ class AppInitializer {
'mode'
:
'mini'
,
'mode'
:
'mini'
,
'iconNavigationColor'
:
'#000000'
,
'iconNavigationColor'
:
'#000000'
,
'navigationColor'
:
'#FFFFFF'
,
'navigationColor'
:
'#FFFFFF'
,
'iconNavigationPosision'
:
'right'
,
//
'iconNavigationPosision': 'right',
'headerTitle'
:
'MyPoint'
,
'headerTitle'
:
'MyPoint'
,
// 'headerSubTitle': 'Tích điểm - đổi quà nhanh chóng',
// 'headerSubTitle': 'Tích điểm - đổi quà nhanh chóng',
//
'headerColor': '#
ffffff
',
'headerColor'
:
'#
E71D28
'
,
// 'headerTextColor': '#000000',
// 'headerTextColor': '#000000',
// 'headerIcon': 'https://cdn.mypoint.vn/app_assets/mypoint_icon.png',
// 'headerIcon': 'https://cdn.mypoint.vn/app_assets/mypoint_icon.png',
});
});
if
(
response
!=
null
&&
kDebugMode
)
{
if
(
response
!=
null
)
{
p
rint
(
'🧭 x-app-sdk header configured:
$response
'
);
debugP
rint
(
'🧭 x-app-sdk header configured:
$response
'
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
p
rint
(
'❌ Failed to configure x-app-sdk header:
$error
'
);
debugP
rint
(
'❌ Failed to configure x-app-sdk header:
$error
'
);
}
}
}
}
...
@@ -106,7 +106,7 @@ class AppInitializer {
...
@@ -106,7 +106,7 @@ class AppInitializer {
// Handle launch from local notification tap when app was killed
// Handle launch from local notification tap when app was killed
handleLocalNotificationLaunchIfAny
();
handleLocalNotificationLaunchIfAny
();
}
catch
(
e
)
{
}
catch
(
e
)
{
if
(
kDebugMode
)
p
rint
(
'Error in setupPostInitCallbacks:
$e
'
);
debugP
rint
(
'Error in setupPostInitCallbacks:
$e
'
);
}
}
}
}
...
@@ -114,9 +114,7 @@ class AppInitializer {
...
@@ -114,9 +114,7 @@ class AppInitializer {
static
Future
<
void
>
_handleInitialNotificationLaunch
()
async
{
static
Future
<
void
>
_handleInitialNotificationLaunch
()
async
{
try
{
try
{
final
initial
=
await
FirebaseMessaging
.
instance
.
getInitialMessage
();
final
initial
=
await
FirebaseMessaging
.
instance
.
getInitialMessage
();
print
(
debugPrint
(
'Checking initial message for app launch from terminated state...
$initial
'
);
'Checking initial message for app launch from terminated state...
$initial
'
,
);
if
(
initial
==
null
)
return
;
if
(
initial
==
null
)
return
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
Future
.
delayed
(
const
Duration
(
seconds:
1
),
()
{
Future
.
delayed
(
const
Duration
(
seconds:
1
),
()
{
...
...
lib/core/deep_link_service.dart
View file @
a6797435
...
@@ -5,7 +5,6 @@ import 'package:mypoint_flutter_app/extensions/string_extension.dart';
...
@@ -5,7 +5,6 @@ import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import
'package:uni_links/uni_links.dart'
;
import
'package:uni_links/uni_links.dart'
;
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
import
'package:mypoint_flutter_app/extensions/crypto.dart'
as
mycrypto
;
import
'package:mypoint_flutter_app/extensions/crypto.dart'
as
mycrypto
;
import
'../directional/directional_action_type.dart'
;
import
'../directional/directional_action_type.dart'
;
class
DeepLinkService
{
class
DeepLinkService
{
...
@@ -20,7 +19,7 @@ class DeepLinkService {
...
@@ -20,7 +19,7 @@ class DeepLinkService {
Future
<
void
>
initialize
()
async
{
Future
<
void
>
initialize
()
async
{
if
(
_initialized
)
return
;
if
(
_initialized
)
return
;
_initialized
=
true
;
_initialized
=
true
;
if
(
kDebugMode
)
p
rint
(
'🔗 Initializing DeepLinkService...'
);
debugP
rint
(
'🔗 Initializing DeepLinkService...'
);
await
_initBranchSdk
();
await
_initBranchSdk
();
await
_handleInitialLink
();
await
_handleInitialLink
();
...
@@ -38,21 +37,15 @@ class DeepLinkService {
...
@@ -38,21 +37,15 @@ class DeepLinkService {
Future
<
void
>
_initBranchSdk
()
async
{
Future
<
void
>
_initBranchSdk
()
async
{
try
{
try
{
await
FlutterBranchSdk
.
init
(
enableLogging:
kDebugMode
);
await
FlutterBranchSdk
.
init
(
enableLogging:
kDebugMode
);
if
(
kDebugMode
)
{
debugPrint
(
'🌿 Branch SDK init '
);
print
(
'🌿 Branch SDK init '
);
}
_branchSub
=
FlutterBranchSdk
.
listSession
().
listen
(
_branchSub
=
FlutterBranchSdk
.
listSession
().
listen
(
_handleBranchSession
,
_handleBranchSession
,
onError:
(
error
)
{
onError:
(
error
)
{
if
(
kDebugMode
)
{
debugPrint
(
'❌ Branch session stream error:
$error
'
);
print
(
'❌ Branch session stream error:
$error
'
);
}
},
},
);
);
}
catch
(
e
)
{
}
catch
(
e
)
{
if
(
kDebugMode
)
{
debugPrint
(
'❌ Failed to initialize Branch SDK:
$e
'
);
print
(
'❌ Failed to initialize Branch SDK:
$e
'
);
}
}
}
}
}
...
@@ -75,7 +68,7 @@ class DeepLinkService {
...
@@ -75,7 +68,7 @@ class DeepLinkService {
// Firebase Dynamic Links removed due to version constraints.
// Firebase Dynamic Links removed due to version constraints.
void
_routeFromUriString
(
String
uriStr
)
{
void
_routeFromUriString
(
String
uriStr
)
{
if
(
kDebugMode
)
p
rint
(
'🔗 Deep link received:
$uriStr
'
);
debugP
rint
(
'🔗 Deep link received:
$uriStr
'
);
final
uri
=
Uri
.
tryParse
(
uriStr
);
final
uri
=
Uri
.
tryParse
(
uriStr
);
if
(
uri
==
null
)
return
;
if
(
uri
==
null
)
return
;
...
@@ -90,7 +83,7 @@ class DeepLinkService {
...
@@ -90,7 +83,7 @@ class DeepLinkService {
for
(
final
secret
in
candidates
)
{
for
(
final
secret
in
candidates
)
{
final
phone
=
mycrypto
.
Crypto
(
cipherHex:
cipherHex
,
secretKey:
secret
).
decryption
().
orEmpty
;
final
phone
=
mycrypto
.
Crypto
(
cipherHex:
cipherHex
,
secretKey:
secret
).
decryption
().
orEmpty
;
if
(
phone
.
isNotEmpty
)
{
if
(
phone
.
isNotEmpty
)
{
if
(
kDebugMode
)
p
rint
(
'🔐 Decrypted phone from key:
$phone
'
);
debugP
rint
(
'🔐 Decrypted phone from key:
$phone
'
);
final
direction
=
DirectionalScreen
.
buildByName
(
final
direction
=
DirectionalScreen
.
buildByName
(
name:
DirectionalScreenName
.
linkMBPAccount
,
name:
DirectionalScreenName
.
linkMBPAccount
,
clickActionParam:
phone
clickActionParam:
phone
...
@@ -108,9 +101,7 @@ class DeepLinkService {
...
@@ -108,9 +101,7 @@ class DeepLinkService {
}
}
void
_handleBranchSession
(
Map
<
dynamic
,
dynamic
>
data
)
{
void
_handleBranchSession
(
Map
<
dynamic
,
dynamic
>
data
)
{
if
(
kDebugMode
)
{
debugPrint
(
'🌿 Branch session data:
$data
'
);
print
(
'🌿 Branch session data:
$data
'
);
}
final
dynamic
clickedLink
=
data
[
"+clicked_branch_link"
];
final
dynamic
clickedLink
=
data
[
"+clicked_branch_link"
];
if
(
clickedLink
!=
true
&&
clickedLink
!=
"true"
)
{
if
(
clickedLink
!=
true
&&
clickedLink
!=
"true"
)
{
...
...
lib/deferred/deferred_routes.dart
0 → 100644
View file @
a6797435
This diff is collapsed.
Click to expand it.
lib/directional/directional_action_type.dart
View file @
a6797435
import
'../shared/router_gage.dart'
;
enum
DirectionalScreenName
{
enum
DirectionalScreenName
{
flashSale
,
flashSale
,
searchProduct
,
searchProduct
,
...
...
lib/directional/directional_screen.dart
View file @
a6797435
...
@@ -35,11 +35,14 @@ class DirectionalScreen {
...
@@ -35,11 +35,14 @@ class DirectionalScreen {
DirectionalScreen
.
_
({
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
popup
});
DirectionalScreen
.
_
({
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
popup
});
factory
DirectionalScreen
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
DirectionalScreen
.
_
(
factory
DirectionalScreen
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
DirectionalScreen
.
_
(
clickActionType:
json
[
Defines
.
action
T
ype
]
as
String
?,
clickActionType:
json
[
'click_
action
_t
ype
'
]
as
String
?,
clickActionParam:
json
[
Defines
.
action
P
aram
s
]
as
String
?,
clickActionParam:
json
[
'click_
action
_p
aram
'
]
as
String
?,
);
);
Map
<
String
,
dynamic
>
toJson
()
=>
{
Defines
.
actionType
:
clickActionType
,
Defines
.
actionParams
:
clickActionParam
};
Map
<
String
,
dynamic
>
toJson
()
=>
{
'click_action_type'
:
clickActionType
,
'click_action_param'
:
clickActionParam
,
};
static
DirectionalScreen
?
build
({
String
?
clickActionType
,
String
?
clickActionParam
})
{
static
DirectionalScreen
?
build
({
String
?
clickActionType
,
String
?
clickActionParam
})
{
if
((
clickActionType
??
""
).
isEmpty
)
return
null
;
if
((
clickActionType
??
""
).
isEmpty
)
return
null
;
...
@@ -58,13 +61,8 @@ class DirectionalScreen {
...
@@ -58,13 +61,8 @@ class DirectionalScreen {
bool
begin
()
{
bool
begin
()
{
final
type
=
DirectionalScreenNameExtension
.
fromRawValue
(
clickActionType
??
""
);
final
type
=
DirectionalScreenNameExtension
.
fromRawValue
(
clickActionType
??
""
);
if
(
type
==
null
)
{
print
(
"Không nhận diện được action type:
$clickActionType
"
);
return
false
;
}
switch
(
type
)
{
switch
(
type
)
{
case
DirectionalScreenName
.
flashSale
:
case
DirectionalScreenName
.
flashSale
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
flashSaleScreen
,
arguments:
{
"groupId"
:
clickActionParam
});
Get
.
toNamed
(
flashSaleScreen
,
arguments:
{
"groupId"
:
clickActionParam
});
return
true
;
return
true
;
case
DirectionalScreenName
.
brand
:
case
DirectionalScreenName
.
brand
:
...
@@ -152,9 +150,11 @@ class DirectionalScreen {
...
@@ -152,9 +150,11 @@ class DirectionalScreen {
final
body
=
Uri
.
encodeComponent
(
contentDecoded
);
final
body
=
Uri
.
encodeComponent
(
contentDecoded
);
// iOS: &body=..., Android: ?body=...
// iOS: &body=..., Android: ?body=...
final
isIOS
=
defaultTargetPlatform
==
TargetPlatform
.
iOS
;
final
isIOS
=
defaultTargetPlatform
==
TargetPlatform
.
iOS
;
final
urlStr
=
isIOS
?
'sms:
$phone
&body=
$body
'
:
'sms:
$phone
?body=
$body
'
;
final
urlStr
=
isIOS
?
'sms:
$phone
&body=
$body
'
:
'sms:
$phone
?body=
$body
'
;
final
uri
=
Uri
.
parse
(
urlStr
);
final
uri
=
Uri
.
parse
(
urlStr
);
p
rint
(
'Mở SMS:
$uri
phone=
$phone
, content=
$content
'
);
debugP
rint
(
'Mở SMS:
$uri
phone=
$phone
, content=
$content
'
);
_openUrlExternally
(
uri
);
_openUrlExternally
(
uri
);
return
false
;
return
false
;
case
DirectionalScreenName
.
setting
:
case
DirectionalScreenName
.
setting
:
...
@@ -170,13 +170,15 @@ class DirectionalScreen {
...
@@ -170,13 +170,15 @@ class DirectionalScreen {
BaseWebViewInput
input
=
BaseWebViewInput
(
url:
clickActionParam
??
""
);
BaseWebViewInput
input
=
BaseWebViewInput
(
url:
clickActionParam
??
""
);
Get
.
toNamed
(
baseWebViewScreen
,
arguments:
input
);
Get
.
toNamed
(
baseWebViewScreen
,
arguments:
input
);
return
true
;
return
true
;
case
DirectionalScreenName
.
website
:
case
DirectionalScreenName
.
website
||
DirectionalScreenName
.
introduction
:
Get
.
toNamed
(
campaignDetailScreen
,
arguments:
{
"id"
:
clickActionParam
??
""
});
Get
.
toNamed
(
campaignDetailScreen
,
arguments:
{
"id"
:
clickActionParam
??
""
});
return
true
;
return
true
;
case
DirectionalScreenName
.
gameCenter
:
case
DirectionalScreenName
.
gameCenter
:
Get
.
toNamed
(
vplayGameCenterScreen
);
Get
.
toNamed
(
vplayGameCenterScreen
);
return
true
;
return
true
;
case
DirectionalScreenName
.
viewAllVoucher
:
case
DirectionalScreenName
.
viewAllVoucher
||
DirectionalScreenName
.
searchProduct
||
DirectionalScreenName
.
productVoucher
:
Get
.
toNamed
(
vouchersScreen
,
arguments:
{
"enableSearch"
:
true
});
Get
.
toNamed
(
vouchersScreen
,
arguments:
{
"enableSearch"
:
true
});
return
true
;
return
true
;
case
DirectionalScreenName
.
news
:
case
DirectionalScreenName
.
news
:
...
@@ -307,7 +309,7 @@ class DirectionalScreen {
...
@@ -307,7 +309,7 @@ class DirectionalScreen {
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
_handleLinkMBPAccount
();
_handleLinkMBPAccount
();
return
true
;
return
true
;
case
DirectionalScreenName
.
notifications
:
case
DirectionalScreenName
.
notifications
:
Get
.
toNamed
(
notificationScreen
);
Get
.
toNamed
(
notificationScreen
);
return
true
;
return
true
;
case
DirectionalScreenName
.
notification
:
case
DirectionalScreenName
.
notification
:
...
@@ -342,7 +344,11 @@ class DirectionalScreen {
...
@@ -342,7 +344,11 @@ class DirectionalScreen {
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
affiliateBrandDetailScreen
,
arguments:
{
"brandId"
:
clickActionParam
});
Get
.
toNamed
(
affiliateBrandDetailScreen
,
arguments:
{
"brandId"
:
clickActionParam
});
return
true
;
return
true
;
case
DirectionalScreenName
.
viewGift
||
// case DirectionalScreenName.historyInvitedFriend:
// case DirectionalScreenName.screenAddInvitationCode:
// Deprecated / unsupported entries kept in enum for backward compatibility.
case
DirectionalScreenName
.
flashSale
||
DirectionalScreenName
.
viewGift
||
DirectionalScreenName
.
feedback
||
DirectionalScreenName
.
feedback
||
DirectionalScreenName
.
ranking
||
DirectionalScreenName
.
ranking
||
DirectionalScreenName
.
inputReferralCode
||
DirectionalScreenName
.
inputReferralCode
||
...
@@ -361,13 +367,13 @@ class DirectionalScreen {
...
@@ -361,13 +367,13 @@ class DirectionalScreen {
_logUnsupported
(
type
);
_logUnsupported
(
type
);
return
false
;
return
false
;
default
:
default
:
p
rint
(
"Không nhận diện được action type:
$clickActionType
"
);
debugP
rint
(
"Không nhận diện được action type:
$clickActionType
"
);
return
false
;
return
false
;
}
}
}
}
void
_logUnsupported
(
DirectionalScreenName
type
)
{
void
_logUnsupported
(
type
)
{
p
rint
(
"⚠️
Chưa hỗ trợ điều hướng action type
:
${type.rawValue}
"
);
debugP
rint
(
"⚠️
DirectionalScreen action type không được hỗ trợ
:
${type.rawValue}
"
);
}
}
void
_handleLinkMBPAccount
()
{
void
_handleLinkMBPAccount
()
{
...
@@ -390,7 +396,7 @@ class DirectionalScreen {
...
@@ -390,7 +396,7 @@ class DirectionalScreen {
onPressed:
()
async
{
onPressed:
()
async
{
Get
.
back
();
Get
.
back
();
_gotoLoginScreen
(
phone
,
password
);
_gotoLoginScreen
(
phone
,
password
);
p
rint
(
"Đồng ý đăng xuất để liên kết tài khoản
$phone
"
);
debugP
rint
(
"Đồng ý đăng xuất để liên kết tài khoản
$phone
"
);
},
},
bgColor:
BaseColor
.
primary500
,
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
textColor:
Colors
.
white
,
...
@@ -406,17 +412,10 @@ class DirectionalScreen {
...
@@ -406,17 +412,10 @@ class DirectionalScreen {
await
DataPreference
.
instance
.
clearData
();
await
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
loginScreen
,
arguments:
{
"phone"
:
phone
,
'password'
:
password
});
Get
.
offAllNamed
(
loginScreen
,
arguments:
{
"phone"
:
phone
,
'password'
:
password
});
}
}
Future
<
void
>
openSystemSettings
()
async
{
final
opened
=
await
openAppSettings
();
if
(!
opened
)
{
debugPrint
(
'⚠️ Không mở được trang cài đặt hệ thống'
);
}
}
}
}
Future
<
bool
>
forceOpen
({
required
Uri
url
,
LaunchMode
mode
=
LaunchMode
.
platformDefault
})
async
{
Future
<
bool
>
forceOpen
({
required
Uri
url
,
LaunchMode
mode
=
LaunchMode
.
platformDefault
})
async
{
p
rint
(
"force open
${url.toString()}
"
);
debugP
rint
(
"force open
${url.toString()}
"
);
if
(
await
canLaunchUrl
(
url
))
{
if
(
await
canLaunchUrl
(
url
))
{
await
launchUrl
(
await
launchUrl
(
url
,
url
,
...
@@ -461,7 +460,10 @@ Future<bool> _safeOpenUrl(Uri url, {LaunchMode preferred = LaunchMode.platformDe
...
@@ -461,7 +460,10 @@ Future<bool> _safeOpenUrl(Uri url, {LaunchMode preferred = LaunchMode.platformDe
final
ok
=
await
launchUrl
(
final
ok
=
await
launchUrl
(
url
,
url
,
mode:
mode
,
mode:
mode
,
webViewConfiguration:
const
WebViewConfiguration
(
enableJavaScript:
true
,
headers:
<
String
,
String
>{}),
webViewConfiguration:
const
WebViewConfiguration
(
enableJavaScript:
true
,
headers:
<
String
,
String
>{},
),
);
);
if
(
ok
)
return
true
;
if
(
ok
)
return
true
;
}
catch
(
_
)
{
}
catch
(
_
)
{
...
...
lib/env_loader.dart
View file @
a6797435
...
@@ -25,9 +25,7 @@ class AppConfig {
...
@@ -25,9 +25,7 @@ class AppConfig {
if
((
flavor
??
''
).
isEmpty
)
throw
const
FormatException
(
"env thiếu 'flavor'"
);
if
((
flavor
??
''
).
isEmpty
)
throw
const
FormatException
(
"env thiếu 'flavor'"
);
if
((
baseUrl
??
''
).
isEmpty
)
throw
const
FormatException
(
"env thiếu 'baseUrl'"
);
if
((
baseUrl
??
''
).
isEmpty
)
throw
const
FormatException
(
"env thiếu 'baseUrl'"
);
// if ((token ?? '').isEmpty) throw const FormatException("env thiếu 'libToken'");
// if ((token ?? '').isEmpty) throw const FormatException("env thiếu 'libToken'");
if
(
kDebugMode
)
{
debugPrint
(
'AppConfig: flavor=
$flavor
, baseUrl=
$baseUrl
, t3Token=
${token ?? ''}
, enableLogging=
$enable
'
);
print
(
'AppConfig: flavor=
$flavor
, baseUrl=
$baseUrl
, t3Token=
${token ?? ''}
, enableLogging=
$enable
'
);
}
return
AppConfig
(
return
AppConfig
(
flavor:
flavor
!,
flavor:
flavor
!,
baseUrl:
baseUrl
!,
baseUrl:
baseUrl
!,
...
@@ -57,9 +55,7 @@ Future<void> loadEnv() async {
...
@@ -57,9 +55,7 @@ Future<void> loadEnv() async {
throw
Exception
(
'Không tải được cấu hình môi trường (env).'
);
throw
Exception
(
'Không tải được cấu hình môi trường (env).'
);
}
}
AppConfig
.
current
=
AppConfig
.
fromMap
(
cfg
);
AppConfig
.
current
=
AppConfig
.
fromMap
(
cfg
);
if
(
kDebugMode
)
{
debugPrint
(
'✅ AppConfig loaded: flavor=
${AppConfig.current.flavor}
, baseUrl=
${AppConfig.current.baseUrl}
'
);
print
(
'✅ AppConfig loaded: flavor=
${AppConfig.current.flavor}
, baseUrl=
${AppConfig.current.baseUrl}
'
);
}
}
}
Future
<
Map
<
String
,
dynamic
>?>
_tryLoadAsset
(
String
path
)
async
{
Future
<
Map
<
String
,
dynamic
>?>
_tryLoadAsset
(
String
path
)
async
{
...
...
lib/extensions/crypto.dart
View file @
a6797435
import
'dart:convert'
;
import
'dart:typed_data'
;
import
'dart:typed_data'
;
import
'package:encrypt/encrypt.dart'
as
enc
;
import
'package:flutter/cupertino.dart'
;
import
'package:pointycastle/api.dart'
show
KeyParameter
,
PaddedBlockCipher
,
PaddedBlockCipherParameters
;
import
'package:pointycastle/block/aes_fast.dart'
;
import
'package:pointycastle/block/modes/ecb.dart'
;
import
'package:pointycastle/padded_block_cipher/padded_block_cipher_impl.dart'
;
import
'package:pointycastle/paddings/pkcs7.dart'
;
/// Giống Swift:
/// Giống Swift:
/// - `cipherHex`: chuỗi hex của dữ liệu đã mã hoá AES
/// - `cipherHex`: chuỗi hex của dữ liệu đã mã hoá AES
...
@@ -16,17 +23,15 @@ class Crypto {
...
@@ -16,17 +23,15 @@ class Crypto {
final
keyBytes
=
_normalizeKeyUtf8
(
secretKey
,
16
);
// AES-128 = 16 bytes
final
keyBytes
=
_normalizeKeyUtf8
(
secretKey
,
16
);
// AES-128 = 16 bytes
final
dataBytes
=
_hexToBytes
(
cipherHex
);
final
dataBytes
=
_hexToBytes
(
cipherHex
);
final
key
=
enc
.
Key
(
keyBytes
);
final
cipher
=
_createCipher
(
keyBytes
);
final
aes
=
enc
.
AES
(
key
,
mode:
enc
.
AESMode
.
ecb
,
padding:
'PKCS7'
);
final
decryptedBytes
=
cipher
.
process
(
dataBytes
);
final
encrypter
=
enc
.
Encrypter
(
aes
);
final
decrypted
=
utf8
.
decode
(
decryptedBytes
);
final
decrypted
=
encrypter
.
decrypt
(
enc
.
Encrypted
(
dataBytes
));
// ignore: avoid_print
// ignore: avoid_print
p
rint
(
'Decrypted Text:
$decrypted
'
);
debugP
rint
(
'Decrypted Text:
$decrypted
'
);
return
decrypted
;
return
decrypted
;
}
catch
(
e
)
{
}
catch
(
e
)
{
// ignore: avoid_print
// ignore: avoid_print
p
rint
(
'Decryption failed:
$e
'
);
debugP
rint
(
'Decryption failed:
$e
'
);
return
null
;
return
null
;
}
}
}
}
...
@@ -54,4 +59,17 @@ class Crypto {
...
@@ -54,4 +59,17 @@ class Crypto {
out
.
setRange
(
0
,
raw
.
length
,
raw
);
out
.
setRange
(
0
,
raw
.
length
,
raw
);
return
out
;
return
out
;
}
}
PaddedBlockCipher
_createCipher
(
Uint8List
key
)
{
final
engine
=
ECBBlockCipher
(
AESFastEngine
());
final
cipher
=
PaddedBlockCipherImpl
(
PKCS7Padding
(),
engine
);
cipher
.
init
(
false
,
PaddedBlockCipherParameters
<
KeyParameter
,
Null
>(
KeyParameter
(
key
),
null
,
),
);
return
cipher
;
}
}
}
lib/extensions/string_extension.dart
View file @
a6797435
import
'dart:convert'
;
import
'dart:convert'
;
import
'package:crypto/crypto.dart'
;
import
'package:crypto/crypto.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:intl/intl.dart'
as
intl
;
import
'package:intl/intl.dart'
as
intl
;
...
@@ -81,7 +82,7 @@ extension StringDateExtension on String {
...
@@ -81,7 +82,7 @@ extension StringDateExtension on String {
try
{
try
{
return
DateTime
.
parse
(
this
);
// 🚀 Dùng Dart core parse luôn đúng
return
DateTime
.
parse
(
this
);
// 🚀 Dùng Dart core parse luôn đúng
}
catch
(
e
)
{
}
catch
(
e
)
{
p
rint
(
'❌ Date parse failed for "
$this
":
$e
'
);
debugP
rint
(
'❌ Date parse failed for "
$this
":
$e
'
);
return
null
;
return
null
;
}
}
}
}
...
@@ -91,7 +92,7 @@ extension StringDateExtension on String {
...
@@ -91,7 +92,7 @@ extension StringDateExtension on String {
try
{
try
{
return
intl
.
DateFormat
(
format
).
parseStrict
(
this
);
return
intl
.
DateFormat
(
format
).
parseStrict
(
this
);
}
catch
(
e
)
{
}
catch
(
e
)
{
p
rint
(
'❌ Date parse failed for "
$this
" with format "
$format
":
$e
'
);
debugP
rint
(
'❌ Date parse failed for "
$this
" with format "
$format
":
$e
'
);
return
null
;
return
null
;
}
}
}
}
...
@@ -119,7 +120,7 @@ extension ParseInt on String {
...
@@ -119,7 +120,7 @@ extension ParseInt on String {
final
doubleValue
=
double
.
parse
(
this
);
final
doubleValue
=
double
.
parse
(
this
);
return
doubleValue
%
1
==
0
?
doubleValue
.
toInt
()
:
doubleValue
;
return
doubleValue
%
1
==
0
?
doubleValue
.
toInt
()
:
doubleValue
;
}
catch
(
e
)
{
}
catch
(
e
)
{
p
rint
(
'❌ String to Int parse failed for "
$this
":
$e
'
);
debugP
rint
(
'❌ String to Int parse failed for "
$this
":
$e
'
);
return
0
;
return
0
;
}
}
}
}
...
...
lib/firebase/push_notification.dart
View file @
a6797435
...
@@ -12,10 +12,8 @@ class NotificationRouter {
...
@@ -12,10 +12,8 @@ class NotificationRouter {
}
}
static
Future
<
void
>
handleDirectionNotification
(
PushNotification
notification
)
async
{
static
Future
<
void
>
handleDirectionNotification
(
PushNotification
notification
)
async
{
if
(
kDebugMode
)
{
debugPrint
(
'Parsed action type:
${notification.screenDirectional?.clickActionType}
'
);
print
(
'Parsed action type:
${notification.screenDirectional?.clickActionType}
'
);
debugPrint
(
'Parsed action param:
${notification.screenDirectional?.clickActionParam}
'
);
print
(
'Parsed action param:
${notification.screenDirectional?.clickActionParam}
'
);
}
notification
.
screenDirectional
?.
begin
();
notification
.
screenDirectional
?.
begin
();
}
}
}
}
...
@@ -50,40 +48,36 @@ class PushNotification {
...
@@ -50,40 +48,36 @@ class PushNotification {
String
?
get
id
=>
_asString
(
info
[
'notification_id'
]);
String
?
get
id
=>
_asString
(
info
[
'notification_id'
]);
DirectionalScreen
?
get
screenDirectional
{
DirectionalScreen
?
get
screenDirectional
{
if
(
kDebugMode
)
{
debugPrint
(
'=== PARSING NOTIFICATION DATA ==='
);
print
(
'=== PARSING NOTIFICATION DATA ==='
);
debugPrint
(
'Raw info:
$info
'
);
print
(
'Raw info:
$info
'
);
}
String
?
name
;
String
?
name
;
String
?
param
;
String
?
param
;
final
extra
=
_asMap
(
info
[
'app_extra'
]);
final
extra
=
_asMap
(
info
[
'app_extra'
]);
if
(
kDebugMode
)
p
rint
(
'App extra:
$extra
'
);
debugP
rint
(
'App extra:
$extra
'
);
final
screenData
=
_asMap
(
extra
?[
'screenData'
]);
final
screenData
=
_asMap
(
extra
?[
'screenData'
]);
if
(
kDebugMode
)
p
rint
(
'Screen data:
$screenData
'
);
debugP
rint
(
'Screen data:
$screenData
'
);
if
(
screenData
!=
null
)
{
if
(
screenData
!=
null
)
{
name
=
_asString
(
screenData
[
Defines
.
actionType
]);
name
=
_asString
(
screenData
[
Defines
.
actionType
]);
param
=
_asString
(
screenData
[
Defines
.
actionParams
]);
param
=
_asString
(
screenData
[
Defines
.
actionParams
]);
if
(
kDebugMode
)
p
rint
(
'From screenData - name:
$name
, param:
$param
'
);
debugP
rint
(
'From screenData - name:
$name
, param:
$param
'
);
}
else
{
}
else
{
name
=
_asString
(
info
[
Defines
.
actionType
]);
name
=
_asString
(
info
[
Defines
.
actionType
]);
param
=
_asString
(
info
[
Defines
.
actionParams
]);
param
=
_asString
(
info
[
Defines
.
actionParams
]);
if
(
kDebugMode
)
p
rint
(
'From info - name:
$name
, param:
$param
'
);
debugP
rint
(
'From info - name:
$name
, param:
$param
'
);
}
}
DirectionalScreen
?
screen
;
DirectionalScreen
?
screen
;
if
(
name
!=
null
||
param
!=
null
)
{
if
(
name
!=
null
||
param
!=
null
)
{
if
(
kDebugMode
)
p
rint
(
'Building DirectionalScreen with name:
$name
, param:
$param
'
);
debugP
rint
(
'Building DirectionalScreen with name:
$name
, param:
$param
'
);
screen
=
DirectionalScreen
.
build
(
clickActionType:
name
,
clickActionParam:
param
);
screen
=
DirectionalScreen
.
build
(
clickActionType:
name
,
clickActionParam:
param
);
}
}
if
(
kDebugMode
)
{
debugPrint
(
'No action data found, using default notification screen with ID:
$id
'
);
print
(
'No action data found, using default notification screen with ID:
$id
'
);
debugPrint
(
'Title:
$title
, Body:
$body
'
);
print
(
'Title:
$title
, Body:
$body
'
);
}
screen
??=
DirectionalScreen
.
buildByName
(
name:
DirectionalScreenName
.
notification
,
clickActionParam:
id
);
screen
??=
DirectionalScreen
.
buildByName
(
name:
DirectionalScreenName
.
notification
,
clickActionParam:
id
);
screen
?.
extraData
=
{
screen
?.
extraData
=
{
'title'
:
title
,
'title'
:
title
,
...
...
lib/firebase/push_setup.dart
View file @
a6797435
...
@@ -11,7 +11,7 @@ import 'notification_parse_payload.dart';
...
@@ -11,7 +11,7 @@ import 'notification_parse_payload.dart';
@pragma
(
'vm:entry-point'
)
// bắt buộc cho background isolate
@pragma
(
'vm:entry-point'
)
// bắt buộc cho background isolate
Future
<
void
>
_firebaseMessagingBackgroundHandler
(
RemoteMessage
message
)
async
{
Future
<
void
>
_firebaseMessagingBackgroundHandler
(
RemoteMessage
message
)
async
{
await
Firebase
.
initializeApp
(
options:
DefaultFirebaseOptions
.
currentPlatform
);
await
Firebase
.
initializeApp
(
options:
DefaultFirebaseOptions
.
currentPlatform
);
p
rint
(
'_firebaseMessagingBackgroundHandler
${message.toMap()}
'
);
debugP
rint
(
'_firebaseMessagingBackgroundHandler
${message.toMap()}
'
);
// Android: data-only message sẽ không tự hiển thị. Tự show local notification
// Android: data-only message sẽ không tự hiển thị. Tự show local notification
if
(!
kIsWeb
&&
Platform
.
isAndroid
)
{
if
(!
kIsWeb
&&
Platform
.
isAndroid
)
{
...
@@ -68,7 +68,7 @@ Future<void> _initLocalNotifications() async {
...
@@ -68,7 +68,7 @@ Future<void> _initLocalNotifications() async {
await
_flnp
.
initialize
(
await
_flnp
.
initialize
(
init
,
init
,
onDidReceiveNotificationResponse:
(
response
)
{
onDidReceiveNotificationResponse:
(
response
)
{
p
rint
(
'Response:
$response
, payload:
${response.payload}
'
);
debugP
rint
(
'Response:
$response
, payload:
${response.payload}
'
);
final
info
=
parseNotificationPayload
(
response
.
payload
);
final
info
=
parseNotificationPayload
(
response
.
payload
);
NotificationRouter
.
handleDirectionNotification
(
PushNotification
(
info:
info
));
NotificationRouter
.
handleDirectionNotification
(
PushNotification
(
info:
info
));
},
},
...
@@ -113,13 +113,9 @@ Future<void> initFirebaseAndFcm() async {
...
@@ -113,13 +113,9 @@ Future<void> initFirebaseAndFcm() async {
// Tắt auto init để tránh plugin tự động thao tác permission trên web
// Tắt auto init để tránh plugin tự động thao tác permission trên web
try
{
try
{
await
FirebaseMessaging
.
instance
.
setAutoInitEnabled
(
false
);
await
FirebaseMessaging
.
instance
.
setAutoInitEnabled
(
false
);
if
(
kDebugMode
)
{
debugPrint
(
'Web: FCM auto-init disabled'
);
print
(
'Web: FCM auto-init disabled'
);
}
}
catch
(
_
)
{}
}
catch
(
_
)
{}
if
(
kDebugMode
)
{
debugPrint
(
'Web: skip requesting notification permission at startup'
);
print
(
'Web: skip requesting notification permission at startup'
);
}
}
else
if
(
Platform
.
isIOS
)
{
}
else
if
(
Platform
.
isIOS
)
{
await
messaging
.
requestPermission
(
alert:
true
,
badge:
true
,
sound:
true
);
await
messaging
.
requestPermission
(
alert:
true
,
badge:
true
,
sound:
true
);
}
else
{
}
else
{
...
@@ -128,9 +124,7 @@ Future<void> initFirebaseAndFcm() async {
...
@@ -128,9 +124,7 @@ Future<void> initFirebaseAndFcm() async {
await
_initLocalNotifications
();
await
_initLocalNotifications
();
}
catch
(
e
)
{
}
catch
(
e
)
{
if
(
kDebugMode
)
{
debugPrint
(
'Firebase initialization error:
$e
'
);
print
(
'Firebase initialization error:
$e
'
);
}
// Continue without Firebase on web if there's an error
// Continue without Firebase on web if there's an error
if
(
kIsWeb
)
{
if
(
kIsWeb
)
{
return
;
return
;
...
@@ -140,12 +134,10 @@ Future<void> initFirebaseAndFcm() async {
...
@@ -140,12 +134,10 @@ Future<void> initFirebaseAndFcm() async {
// Foreground: Android không tự hiển thị -> ta show local notification
// Foreground: Android không tự hiển thị -> ta show local notification
FirebaseMessaging
.
onMessage
.
listen
((
message
)
{
FirebaseMessaging
.
onMessage
.
listen
((
message
)
{
if
(
kDebugMode
)
{
debugPrint
(
'=== FOREGROUND MESSAGE RECEIVED ==='
);
print
(
'=== FOREGROUND MESSAGE RECEIVED ==='
);
debugPrint
(
'Message:
${message.messageId}
'
);
print
(
'Message:
${message.messageId}
'
);
debugPrint
(
'Data:
${message.data}
'
);
print
(
'Data:
${message.data}
'
);
debugPrint
(
'Notification:
${message.notification?.title}
-
${message.notification?.body}
'
);
print
(
'Notification:
${message.notification?.title}
-
${message.notification?.body}
'
);
}
// Only show local notifications on mobile platforms, not web
// Only show local notifications on mobile platforms, not web
if
(!
kIsWeb
)
{
if
(!
kIsWeb
)
{
final
n
=
message
.
notification
;
final
n
=
message
.
notification
;
...
@@ -178,13 +170,9 @@ Future<void> initFirebaseAndFcm() async {
...
@@ -178,13 +170,9 @@ Future<void> initFirebaseAndFcm() async {
if
(
messaging
!=
null
&&
!
kIsWeb
)
{
if
(
messaging
!=
null
&&
!
kIsWeb
)
{
try
{
try
{
final
token
=
await
messaging
.
getToken
();
final
token
=
await
messaging
.
getToken
();
if
(
kDebugMode
)
{
debugPrint
(
'FCM token:
$token
'
);
print
(
'FCM token:
$token
'
);
}
}
catch
(
e
)
{
}
catch
(
e
)
{
if
(
kDebugMode
)
{
debugPrint
(
'Error getting FCM token:
$e
'
);
print
(
'Error getting FCM token:
$e
'
);
}
}
}
}
}
}
}
...
@@ -193,9 +181,7 @@ Future<void> initFirebaseAndFcm() async {
...
@@ -193,9 +181,7 @@ Future<void> initFirebaseAndFcm() async {
Future
<
AuthorizationStatus
?>
requestWebNotificationPermission
()
async
{
Future
<
AuthorizationStatus
?>
requestWebNotificationPermission
()
async
{
// ĐÃ TẮT CHO WEB: Không request permission trên web
// ĐÃ TẮT CHO WEB: Không request permission trên web
if
(
kIsWeb
)
{
if
(
kIsWeb
)
{
if
(
kDebugMode
)
{
debugPrint
(
'Web notifications disabled: skip requesting permission'
);
print
(
'Web notifications disabled: skip requesting permission'
);
}
return
AuthorizationStatus
.
denied
;
return
AuthorizationStatus
.
denied
;
}
}
return
null
;
return
null
;
...
@@ -208,21 +194,15 @@ Future<String?> getFcmTokenIfAvailable() async {
...
@@ -208,21 +194,15 @@ Future<String?> getFcmTokenIfAvailable() async {
if
(
kIsWeb
)
{
if
(
kIsWeb
)
{
final
settings
=
await
messaging
.
getNotificationSettings
();
final
settings
=
await
messaging
.
getNotificationSettings
();
if
(
settings
.
authorizationStatus
!=
AuthorizationStatus
.
authorized
)
{
if
(
settings
.
authorizationStatus
!=
AuthorizationStatus
.
authorized
)
{
if
(
kDebugMode
)
{
debugPrint
(
'Web notifications not authorized. Cannot retrieve FCM token.'
);
print
(
'Web notifications not authorized. Cannot retrieve FCM token.'
);
}
return
null
;
return
null
;
}
}
}
}
final
token
=
await
messaging
.
getToken
();
final
token
=
await
messaging
.
getToken
();
if
(
kDebugMode
)
{
debugPrint
(
'FCM token (if available):
$token
'
);
print
(
'FCM token (if available):
$token
'
);
}
return
token
;
return
token
;
}
catch
(
e
)
{
}
catch
(
e
)
{
if
(
kDebugMode
)
{
debugPrint
(
'Error retrieving FCM token:
$e
'
);
print
(
'Error retrieving FCM token:
$e
'
);
}
return
null
;
return
null
;
}
}
}
}
Prev
1
2
3
4
5
6
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment