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
bfff9e47
Commit
bfff9e47
authored
Nov 10, 2025
by
DatHV
Browse files
update
parent
e838a036
Changes
5
Show whitespace changes
Inline
Side-by-side
export_dev.sh
View file @
bfff9e47
...
@@ -78,13 +78,25 @@ echo "🔨 Flutter build web (release, MAXIMUM optimization for T3 delivery)..."
...
@@ -78,13 +78,25 @@ echo "🔨 Flutter build web (release, MAXIMUM optimization for T3 delivery)..."
# --pwa-strategy=none: Tắt PWA service worker (giảm kích thước)
# --pwa-strategy=none: Tắt PWA service worker (giảm kích thước)
# --no-source-maps: Không tạo source maps (giảm kích thước build)
# --no-source-maps: Không tạo source maps (giảm kích thước build)
# --tree-shake-icons: Chỉ include icons được sử dụng (giảm kích thước fonts)
# --tree-shake-icons: Chỉ include icons được sử dụng (giảm kích thước fonts)
# --dart-define=FLUTTER_WEB_USE_SKIA=false: Force HTML renderer (không dùng CanvasKit)
# --dart-define=FLUTTER_WEB_USE_SKIA: Use CanvasKit if KEEP_CANVASKIT=1, otherwise HTML renderer
flutter build web
\
if
[
"
${
KEEP_CANVASKIT
:-
0
}
"
=
"1"
]
;
then
echo
" → Building with CanvasKit (KEEP_CANVASKIT=1)..."
flutter build web
\
--release
\
--pwa-strategy
=
none
\
--no-source-maps
\
--tree-shake-icons
\
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
true
\
--dart-define
=
FLUTTER_WEB_USE_SKWASM
=
false
else
echo
" → Building with HTML renderer (no CanvasKit)..."
flutter build web
\
--release
\
--release
\
--pwa-strategy
=
none
\
--pwa-strategy
=
none
\
--no-source-maps
\
--no-source-maps
\
--tree-shake-icons
\
--tree-shake-icons
\
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
false
--dart-define
=
FLUTTER_WEB_USE_SKIA
=
false
fi
copy_x_app_sdk
copy_x_app_sdk
...
@@ -111,14 +123,163 @@ content = re.sub(
...
@@ -111,14 +123,163 @@ content = re.sub(
flags=re.IGNORECASE
flags=re.IGNORECASE
)
)
# Ensure flutterConfiguration.renderer is set
# Set renderer and canvasKitBaseUrl based on KEEP_CANVASKIT
if 'window.flutterConfiguration.renderer' not in content:
import os
# Add renderer config before flutter_bootstrap.js loads
keep_canvaskit = os.environ.get('KEEP_CANVASKIT') == '1'
if keep_canvaskit:
# When KEEP_CANVASKIT=1, use CanvasKit with local path ONLY (no CDN fallback)
# Remove all existing CanvasKit config to start fresh
content = re.sub(
r'window
\.
flutterConfiguration
\.
renderer
\s
*=
\s
*[^;]+;',
'',
content
)
content = re.sub(
r'window
\.
flutterConfiguration
\.
canvasKitBaseUrl
\s
*=
\s
*[^;]+;',
'',
content
)
content = re.sub(
r'window
\.
flutterConfiguration
\.
useLocalCanvasKit
\s
*=
\s
*[^;]+;',
'',
content
)
# Ensure flutterConfiguration is initialized FIRST, before any other scripts
# Find the first <script> tag and add config right after it
if not re.search(r'window
\.
flutterConfiguration
\s
*=
\s
*window
\.
flutterConfiguration
\s
*
\|\|\s
*
\{\}
;', content):
# Add initialization if missing - insert right after first script tag or in head
if re.search(r'<script[^>]*>', content):
content = re.sub(
r'(<script[^>]*>)',
r'
\1\n
window.flutterConfiguration = window.flutterConfiguration || {};',
content,
count=1
)
else:
# Add before closing head if no script tag found
content = re.sub(
r'(</head>)',
r' <script>
\n
window.flutterConfiguration = window.flutterConfiguration || {};
\n
</script>
\n\1
',
content
)
# Set CanvasKit config - MUST be set before flutter_bootstrap.js loads
# Use relative path 'canvaskit/' (will resolve to ./canvaskit/ from document base)
# Set useLocalCanvasKit to prevent any CDN fallback
config_block = """ window.flutterConfiguration.renderer = 'canvaskit';
window.flutterConfiguration.canvasKitBaseUrl = 'canvaskit/';
window.flutterConfiguration.useLocalCanvasKit = true;
// Prevent CDN fallback - force local only
window.flutterConfiguration.canvasKitVariant = 'chromium';"""
# Insert config right after flutterConfiguration initialization
if re.search(r'window
\.
flutterConfiguration
\s
*=
\s
*window
\.
flutterConfiguration
\s
*
\|\|\s
*
\{\}
;', content):
content = re.sub(
r'(window
\.
flutterConfiguration
\s
*=
\s
*window
\.
flutterConfiguration
\s
*
\|\|\s
*
\{\}
;)',
r'
\1\n
' + config_block,
content
)
else:
# Fallback: add right after first script tag
content = re.sub(
r'(<script[^>]*>
\s
*[^<]*)',
r'
\1\n
' + config_block,
content,
count=1
)
# Add script to redirect CDN requests to local - MUST run before flutter_bootstrap.js
redirect_cdn_script = """ <!-- Redirect CanvasKit CDN requests to local files -->
<script>
(function() {
// Redirect CDN URLs to local before Flutter loader runs
var originalFetch = window.fetch;
window.fetch = function(input, init) {
try {
var url = '';
if (typeof input === 'string') {
url = input;
} else if (input instanceof Request) {
url = input.url;
} else if (input && typeof input === 'object' && input.url) {
url = input.url;
}
if (url && url.includes('gstatic.com/flutter-canvaskit')) {
// Extract the path after the engine revision
// Pattern: https://www.gstatic.com/flutter-canvaskit/REVISION/path
var match = url.match(/flutter-canvaskit
\/
[^
\/
]+
\/
(.+)
$/
);
if (match) {
var localPath = 'canvaskit/' + match[1];
console.log('🔄 Redirecting CDN to local:', url.split('/').pop(), '->', localPath);
// Redirect to local path
if (typeof input === 'string') {
return originalFetch.call(this, localPath, init);
} else if (input instanceof Request) {
return originalFetch.call(this, new Request(localPath, input));
} else {
var newInput = Object.assign({}, input);
newInput.url = localPath;
return originalFetch.call(this, newInput.url || localPath, newInput);
}
}
}
} catch (e) {
console.error('Error in fetch redirect:', e);
}
return originalFetch.apply(this, arguments);
};
})();
</script>"""
# Add preload links for CanvasKit files to optimize loading
canvaskit_preloads = """ <!-- Preload CanvasKit for faster loading (local only) -->
<link rel="preload" href="canvaskit/chromium/canvaskit.wasm" as="fetch" crossorigin="anonymous">
<link rel="preload" href="canvaskit/chromium/canvaskit.js" as="script" crossorigin="anonymous">"""
# Add redirect CDN script and preloads before flutter_bootstrap.js script tag
if re.search(r'<script[^>]*src=["
\'
]flutter_bootstrap
\.
js["
\'
]', content):
content = re.sub(
r'(<script[^>]*src=["
\'
]flutter_bootstrap
\.
js["
\'
][^>]*>)',
redirect_cdn_script + '
\n
' + canvaskit_preloads + '
\n
' + r'
\1
',
content,
count=1
)
elif '</head>' in content:
# Add before closing head tag
content = re.sub(
r'(</head>)',
redirect_cdn_script + '
\n
' + ' ' + canvaskit_preloads + '
\n
' + r'
\1
',
content
)
else:
# When KEEP_CANVASKIT is not set, use HTML renderer
# Replace existing renderer if it's set to 'canvaskit' (handle both escaped and unescaped quotes)
content = re.sub(
r'window
\.
flutterConfiguration
\.
renderer
\s
*=
\s
*(
\\
?[
\'
"])canvaskit
\1
;',
"window.flutterConfiguration.renderer = 'html';",
content
)
# Add renderer if it doesn't exist
if 'window.flutterConfiguration.renderer' not in content:
content = re.sub(
content = re.sub(
r'(window
\.
flutterConfiguration
\s
*=
\s
*window
\.
flutterConfiguration
\s
*
\|\|\s
*
\{\}
;)',
r'(window
\.
flutterConfiguration
\s
*=
\s
*window
\.
flutterConfiguration
\s
*
\|\|\s
*
\{\}
;)',
r'
\1\n
window.flutterConfiguration.renderer =
\'
html
\'
;',
r'
\1\n
window.flutterConfiguration.renderer =
\'
html
\'
;',
content
content
)
)
# Remove canvasKitBaseUrl and useLocalCanvasKit if they exist (not needed for HTML renderer)
content = re.sub(
r'window
\.
flutterConfiguration
\.
canvasKitBaseUrl
\s
*=
\s
*[^;]+;',
'',
content
)
content = re.sub(
r'window
\.
flutterConfiguration
\.
useLocalCanvasKit
\s
*=
\s
*[^;]+;',
'',
content
)
# Remove duplicate script tags for flutter_bootstrap.js (keep only the last one)
# Remove duplicate script tags for flutter_bootstrap.js (keep only the last one)
script_tags = list(re.finditer(r'<script[^>]*src=["
\'
]flutter_bootstrap
\.
js["
\'
][^>]*>', content, re.IGNORECASE))
script_tags = list(re.finditer(r'<script[^>]*src=["
\'
]flutter_bootstrap
\.
js["
\'
][^>]*>', content, re.IGNORECASE))
...
@@ -229,7 +390,11 @@ echo " 📁 Folder: ${folder_size}"
...
@@ -229,7 +390,11 @@ echo " 📁 Folder: ${folder_size}"
echo
""
echo
""
echo
"📌 Delivery notes for T3:"
echo
"📌 Delivery notes for T3:"
echo
" ✅ All assets are pre-compressed (gzip + brotli)"
echo
" ✅ All assets are pre-compressed (gzip + brotli)"
echo
" ✅ CanvasKit removed (saves ~1.6MB)"
if
[
"
${
KEEP_CANVASKIT
:-
0
}
"
=
"1"
]
;
then
echo
" ✅ CanvasKit included (local, not from CDN)"
else
echo
" ✅ CanvasKit removed (saves ~1.6MB)"
fi
echo
" ✅ Source maps removed (saves space)"
echo
" ✅ Source maps removed (saves space)"
echo
" ✅ PWA disabled (simpler deployment)"
echo
" ✅ PWA disabled (simpler deployment)"
echo
" ✅ Tree-shaken icons (only used icons included)"
echo
" ✅ Tree-shaken icons (only used icons included)"
...
...
lib/firebase/push_setup.dart
View file @
bfff9e47
...
@@ -5,7 +5,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
...
@@ -5,7 +5,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter_local_notifications/flutter_local_notifications.dart'
;
import
'package:flutter_local_notifications/flutter_local_notifications.dart'
;
import
'package:mypoint_flutter_app/firebase/push_notification.dart'
;
import
'package:mypoint_flutter_app/firebase/push_notification.dart'
;
import
'firebase_options.dart'
;
import
'
../core/platform/firebase/
firebase_options.dart'
;
import
'notification_parse_payload.dart'
;
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
...
...
run_dev.sh
View file @
bfff9e47
...
@@ -42,6 +42,54 @@ copy_x_app_sdk() {
...
@@ -42,6 +42,54 @@ copy_x_app_sdk() {
echo
"✅ Copied x-app-sdk bundle."
echo
"✅ Copied x-app-sdk bundle."
}
}
compress_assets_for_dev
()
{
local dir
=
"
${
PROJECT_ROOT
}
/build/web"
echo
"🗜️ Compressing assets for 4G optimization (gzip + brotli)..."
# Gzip compression (level 9 = maximum)
if
command
-v
gzip
>
/dev/null 2>&1
;
then
echo
" → Creating .gz files (gzip -9)..."
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.wasm'
-o
-name
'*.css'
-o
-name
'*.json'
\)
!
-name
'*.gz'
!
-name
'*.br'
-exec
gzip
-9
-kf
{}
\;
echo
" ✅ Gzip compression completed"
else
echo
" ⚠️ gzip not available, skipping .gz artifacts"
fi
# Brotli compression (quality 11 = maximum) - better compression for 4G
if
command
-v
brotli
>
/dev/null 2>&1
;
then
echo
" → Creating .br files (brotli -q 11)..."
find
"
${
dir
}
"
-type
f
\(
-name
'*.js'
-o
-name
'*.wasm'
-o
-name
'*.css'
-o
-name
'*.json'
\)
!
-name
'*.gz'
!
-name
'*.br'
-exec
brotli
-f
-k
-q
11
{}
\;
echo
" ✅ Brotli compression completed"
else
echo
" ⚠️ brotli not available, skipping .br artifacts"
fi
# Show compression stats for canvaskit.wasm if it exists
local
wasm_file
=
"
${
dir
}
/canvaskit/chromium/canvaskit.wasm"
if
[
-f
"
${
wasm_file
}
"
]
;
then
local
original_size
=
$(
stat
-f
%z
"
${
wasm_file
}
"
2>/dev/null
||
stat
-c
%s
"
${
wasm_file
}
"
2>/dev/null
||
echo
"0"
)
local
gz_size
=
0
local
br_size
=
0
if
[
-f
"
${
wasm_file
}
.gz"
]
;
then
gz_size
=
$(
stat
-f
%z
"
${
wasm_file
}
.gz"
2>/dev/null
||
stat
-c
%s
"
${
wasm_file
}
.gz"
2>/dev/null
||
echo
"0"
)
fi
if
[
-f
"
${
wasm_file
}
.br"
]
;
then
br_size
=
$(
stat
-f
%z
"
${
wasm_file
}
.br"
2>/dev/null
||
stat
-c
%s
"
${
wasm_file
}
.br"
2>/dev/null
||
echo
"0"
)
fi
echo
""
echo
"📊 canvaskit.wasm compression stats:"
echo
" Original:
$(
numfmt
--to
=
iec-i
--suffix
=
B
${
original_size
}
2>/dev/null
||
echo
"
${
original_size
}
bytes"
)
"
if
[
"
${
gz_size
}
"
-gt
0
]
;
then
local
gz_ratio
=
$(
echo
"scale=1; (
${
original_size
}
-
${
gz_size
}
) * 100 /
${
original_size
}
"
| bc 2>/dev/null
||
echo
"0"
)
echo
" Gzip:
$(
numfmt
--to
=
iec-i
--suffix
=
B
${
gz_size
}
2>/dev/null
||
echo
"
${
gz_size
}
bytes"
)
(
${
gz_ratio
}
% smaller)"
fi
if
[
"
${
br_size
}
"
-gt
0
]
;
then
local
br_ratio
=
$(
echo
"scale=1; (
${
original_size
}
-
${
br_size
}
) * 100 /
${
original_size
}
"
| bc 2>/dev/null
||
echo
"0"
)
echo
" Brotli:
$(
numfmt
--to
=
iec-i
--suffix
=
B
${
br_size
}
2>/dev/null
||
echo
"
${
br_size
}
bytes"
)
(
${
br_ratio
}
% smaller)"
fi
fi
}
build_web
()
{
build_web
()
{
echo
"📦 Getting Flutter packages..."
echo
"📦 Getting Flutter packages..."
flutter pub get
flutter pub get
...
@@ -71,6 +119,9 @@ class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
...
@@ -71,6 +119,9 @@ class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin')
# Add cache headers for static assets to speed up localhost loading
if self.path.endswith(('.js', '.wasm', '.css', '.woff2', '.png', '.jpg', '.svg')):
self.send_header('Cache-Control', 'public, max-age=3600')
super().end_headers()
super().end_headers()
def do_OPTIONS(self):
def do_OPTIONS(self):
...
@@ -83,6 +134,50 @@ class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
...
@@ -83,6 +134,50 @@ class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
path = os.path.join(path, 'index.html')
path = os.path.join(path, 'index.html')
if not os.path.exists(path):
if not os.path.exists(path):
self.path = '/index.html'
self.path = '/index.html'
path = self.translate_path(self.path)
# Check for compressed versions (gzip/brotli) to optimize for 4G throttling
accept_encoding = self.headers.get('Accept-Encoding', '')
compressed_path = None
encoding = None
if os.path.exists(path):
# Prefer brotli, then gzip
if 'br' in accept_encoding and os.path.exists(path + '.br'):
compressed_path = path + '.br'
encoding = 'br'
elif 'gzip' in accept_encoding and os.path.exists(path + '.gz'):
compressed_path = path + '.gz'
encoding = 'gzip'
if compressed_path and encoding:
# Serve compressed file
try:
with open(compressed_path, 'rb') as f:
content = f.read()
self.send_response(200)
# Set content type based on original file
if path.endswith('.wasm'):
self.send_header('Content-Type', 'application/wasm')
elif path.endswith('.js'):
self.send_header('Content-Type', 'application/javascript')
elif path.endswith('.css'):
self.send_header('Content-Type', 'text/css')
elif path.endswith('.json'):
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Encoding', encoding)
self.send_header('Content-Length', str(len(content)))
self.send_header('Vary', 'Accept-Encoding')
self.end_headers()
self.wfile.write(content)
return
except Exception as e:
print(f"⚠️ Error serving compressed file: {e}")
# Fall through to serve uncompressed
# Serve uncompressed file
return super().do_GET()
return super().do_GET()
def log_message(self, fmt, *args):
def log_message(self, fmt, *args):
...
@@ -121,6 +216,7 @@ kill_port "${PORT}"
...
@@ -121,6 +216,7 @@ kill_port "${PORT}"
set_env_dev
set_env_dev
build_web
build_web
copy_x_app_sdk
copy_x_app_sdk
compress_assets_for_dev
start_server
start_server
trap
cleanup EXIT INT TERM
trap
cleanup EXIT INT TERM
sleep
2
sleep
2
...
...
run_dev_nginx.sh
View file @
bfff9e47
...
@@ -81,7 +81,13 @@ echo ""
...
@@ -81,7 +81,13 @@ echo ""
# Step 1: Build and export
# Step 1: Build and export
echo
"📦 Step 1: Building and exporting..."
echo
"📦 Step 1: Building and exporting..."
./export_dev.sh
# Pass through KEEP_CANVASKIT environment variable if set
if
[
-n
"
${
KEEP_CANVASKIT
:-}
"
]
;
then
echo
" ℹ️ KEEP_CANVASKIT=
${
KEEP_CANVASKIT
}
will be passed to export_dev.sh"
KEEP_CANVASKIT
=
"
${
KEEP_CANVASKIT
}
"
./export_dev.sh
else
./export_dev.sh
fi
echo
""
echo
""
update_nginx_config
update_nginx_config
...
...
web/index.html
View file @
bfff9e47
...
@@ -42,10 +42,6 @@
...
@@ -42,10 +42,6 @@
#app-loader
{
#app-loader
{
position
:
fixed
;
position
:
fixed
;
inset
:
0
;
inset
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
background
:
#ffffff
url('assets/assets/images/splash_screen.webp')
center
/
cover
no-repeat
;
background
:
#ffffff
url('assets/assets/images/splash_screen.webp')
center
/
cover
no-repeat
;
z-index
:
9999
;
z-index
:
9999
;
transition
:
opacity
0.3s
ease
,
visibility
0.3s
ease
;
transition
:
opacity
0.3s
ease
,
visibility
0.3s
ease
;
...
@@ -66,35 +62,6 @@
...
@@ -66,35 +62,6 @@
visibility
:
hidden
;
visibility
:
hidden
;
pointer-events
:
none
;
pointer-events
:
none
;
}
}
.app-loader__spinner
{
width
:
56px
;
height
:
56px
;
border
:
6px
solid
rgba
(
0
,
0
,
0
,
0.1
);
border-top-color
:
#2f80ed
;
border-radius
:
50%
;
animation
:
app-loader-spin
1s
linear
infinite
;
position
:
relative
;
z-index
:
1
;
}
.app-loader__text
{
margin-top
:
20px
;
font-family
:
'Helvetica Neue'
,
Arial
,
sans-serif
;
font-size
:
15px
;
color
:
#3b4a6b
;
letter-spacing
:
0.2px
;
text-align
:
center
;
max-width
:
240px
;
position
:
relative
;
z-index
:
1
;
}
@keyframes
app-loader-spin
{
to
{
transform
:
rotate
(
360deg
);
}
}
</style>
</style>
<!-- x-app-sdk integration -->
<!-- x-app-sdk integration -->
...
@@ -309,10 +276,7 @@
...
@@ -309,10 +276,7 @@
</script>
</script>
</head>
</head>
<body>
<body>
<div
id=
"app-loader"
role=
"status"
aria-live=
"polite"
aria-label=
"Đang khởi động ứng dụng"
>
<div
id=
"app-loader"
aria-hidden=
"true"
></div>
<div
class=
"app-loader__spinner"
></div>
<div
class=
"app-loader__text"
>
Đang khởi động MyPoint...
</div>
</div>
<script
src=
"flutter_bootstrap.js"
async
></script>
<script
src=
"flutter_bootstrap.js"
async
></script>
<script>
<script>
...
@@ -332,7 +296,6 @@
...
@@ -332,7 +296,6 @@
}
}
window
.
addEventListener
(
'
flutter-first-frame
'
,
hideLoader
);
window
.
addEventListener
(
'
flutter-first-frame
'
,
hideLoader
);
// Fallback: ensure loader disappears even if event never fires
window
.
addEventListener
(
'
load
'
,
function
()
{
window
.
addEventListener
(
'
load
'
,
function
()
{
setTimeout
(
hideLoader
,
4000
);
setTimeout
(
hideLoader
,
4000
);
});
});
...
...
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