Commit 956c501c authored by DatHV's avatar DatHV
Browse files

update build x-app-sdk

parent 9bb8aadd
# Close App Integration với x-app-sdk
# Close App Integration
## Tổng quan
......@@ -154,6 +154,5 @@ http://localhost:8080/?token=test123&user={"id":"user123"}
- `lib/web/web_helper_web.dart` - Implementation cho web
- `lib/web/web_helper_stub.dart` - Stub cho non-web platforms
- `lib/web/x_app_sdk_service.dart` - Service gọi JavaScript
- `web/index.html` - JavaScript implementation
- `lib/web/close_app_example.dart` - Ví dụ sử dụng
......@@ -34,10 +34,6 @@
./scripts/open_browser_cors_disabled.sh
```
### **Test x-app-sdk:**
```bash
./scripts/test_x_app_sdk.sh
```
## 📁 **Cấu trúc đơn giản:**
......@@ -53,12 +49,12 @@ flutter_app_mypoint/
│ ├── switch_env.sh # Chuyển đổi môi trường
│ ├── run_web_complete.sh # Chạy web với CORS
│ ├── open_browser_cors_disabled.sh # Mở Chrome với CORS disabled
└── test_x_app_sdk.sh # Test x-app-sdk
└── lib/web/ # x-app-sdk integration
└── test_web.sh # Test web
└── lib/web/ # Web integration
```
## ✅ **Kết quả:**
-**Đơn giản**: Chỉ 2 lệnh chính
-**Dễ nhớ**: `./run_dev.sh``./run_prod.sh`
-**x-app-sdk**: Tích hợp đầy đủ
-**Web**: Tích hợp đầy đủ
-**Sẵn sàng**: Chạy ngay
......@@ -18,12 +18,6 @@
- `./scripts/run_web_complete.sh` - Chạy development với CORS
- `./scripts/export_and_run.sh` - Export + chạy + mở browser
#### **3. Tích hợp x-app-sdk:**
- `lib/web/x_app_sdk_service.dart` - Service chính
- `lib/web/web_helper_web.dart` - Web helper functions
- `lib/web/web_helper_stub.dart` - Stub cho non-web
- `web/index.html` - JavaScript integration
- `X_APP_SDK_INTEGRATION.md` - Hướng dẫn tích hợp
## 🚀 **Cách sử dụng đơn giản:**
......@@ -79,22 +73,6 @@ from origin 'http://localhost:8080' has been blocked by CORS policy
- Cài "CORS Unblock" extension
- Enable khi test
## 🔧 **x-app-sdk Integration:**
### **Chức năng:**
- Lấy token từ Super App: `window.getToken()`
- Lấy user info từ Super App: `window.getInfo('USER_ID')`
- Retry mechanism: Tối đa 3 lần
- Fallback: URL parameters và localStorage
### **Test:**
```bash
# Test với URL parameters
http://localhost:8080?token=abc123&userId=user456
# Test x-app-sdk
./scripts/test_x_app_sdk.sh
```
## 📁 **Cấu trúc đơn giản:**
......@@ -103,20 +81,18 @@ flutter_app_mypoint/
├── assets/config/
│ └── env.json # Config duy nhất
├── lib/web/
│ ├── x_app_sdk_service.dart # x-app-sdk service
│ ├── web_helper_web.dart # Web functions
│ └── web_helper_stub.dart # Stub functions
│ └── (web helper files)
├── web/
│ └── index.html # JavaScript integration
├── export_web.sh # Export script
└── scripts/
├── run_web_complete.sh # Development
├── export_and_run.sh # Export + run
└── test_x_app_sdk.sh # Test x-app-sdk
└── test_web.sh # Test web
```
## 🎯 **Kết quả:**
-**Đơn giản**: Chỉ 1 config file
-**Dễ hiểu**: Không còn phức tạp
-**x-app-sdk**: Tích hợp đầy đủ
-**Web**: Tích hợp đầy đủ
-**Sẵn sàng**: Export và deploy ngay
# X-App-SDK Integration Guide for Mini App
# X-App-SDK Integration Guide
## Tổng quan
Tài liệu này mô tả cách tích hợp mini app với `x-app-sdk` để lấy thông tin token, user từ Super App. Super App đã có sẵn `x-app-sdk`, mini app chỉ cần gọi các method để lấy dữ liệu.
Tài liệu này mô tả cách tích hợp mini app với `x-app-sdk` để lấy token và đóng app từ Super App. Implementation này đơn giản và chỉ sử dụng 2 API chính: `getToken()``closeApp()`.
## Cài đặt
### 1. Build Flutter web app
### 1. Install x-app-sdk
```bash
npm install x-app-sdk@^1.1.2
```
### 2. Build Flutter web app
```bash
flutter build web
......@@ -16,63 +22,37 @@ flutter build web
### Trong Super App
Super App đã có sẵn `x-app-sdk` và cung cấp các method global:
Super App không cần làm gì đặc biệt. x-app-sdk sẽ tự động detect Super App environment và sử dụng các method có sẵn.
```javascript
// Super App đã có sẵn các method này:
// window.getToken() - Lấy token đăng nhập
// window.getInfo(key) - Lấy thông tin người dùng theo key
// Mini app sẽ tự động gọi:
// window.getToken() - Lấy token
// window.getInfo('USER_ID') - Lấy ID người dùng
// window.getInfo('USER_NAME') - Lấy tên người dùng
// window.getInfo('USER_EMAIL') - Lấy email người dùng
// window.getInfo('USER_PHONE') - Lấy số điện thoại
```
**Lưu ý**: Mini app sử dụng x-app-sdk thật từ npm package, không phải mock.
### Trong Mini App (Flutter)
Mini app sẽ tự động lấy dữ liệu từ Super App:
Mini app sẽ tự động lấy token từ Super App khi khởi động:
```dart
// Lấy token
String? token = webGetAppHostToken();
// Token được lấy tự động trong splash screen
// Không cần gọi thủ công
// Lấy user info
Map<String, dynamic>? user = webGetAppHostUser();
// Kiểm tra data có sẵn
bool isReady = webIsAppHostDataReady();
// Kiểm tra x-app-sdk có sẵn từ Super App
bool sdkAvailable = webIsSDKAvailable();
// Lấy error message nếu có
String? error = webGetAppHostError();
// Lấy thông tin user theo key cụ thể
dynamic userInfo = await webGetUserInfoByKey('USER_NAME');
// Lấy token bất đồng bộ
String? token = await webGetTokenAsync();
// Đóng app và trả về Super App
webCloseApp({
'message': 'Task completed',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
```
## API Reference
### Web Helper Functions
- `webGetAppHostToken()`: Lấy token từ Super App
- `webGetAppHostUser()`: Lấy thông tin user từ Super App
- `webIsAppHostDataReady()`: Kiểm tra data có sẵn
- `webIsSDKAvailable()`: Kiểm tra x-app-sdk có sẵn từ Super App
- `webGetAppHostError()`: Lấy error message
- `webInitializeXAppSDK()`: Khởi tạo x-app-sdk service
- `webStoreAppHostData(token, user)`: Lưu data vào localStorage
- `webClearAppHostData()`: Xóa data
- `webCallXAppSDKMethod(methodName, args)`: Gọi method của x-app-sdk
- `webGetUserInfoByKey(key)`: Lấy thông tin user theo key
- `webGetTokenAsync()`: Lấy token bất đồng bộ
- `webGetToken()`: Lấy token từ Super App
- `webCloseApp(data)`: Đóng app và trả về Super App với data
- `webIsSDKInitialized()`: Kiểm tra SDK đã khởi tạo chưa
- `webGetCachedToken()`: Lấy token đã cache
- `webGetLastError()`: Lấy error message cuối cùng
- `webClearTokenCache()`: Xóa token cache
- `webResetSDK()`: Reset SDK service
### XAppSDKService
......@@ -82,105 +62,67 @@ final service = XAppSDKService();
// Khởi tạo
await service.initialize();
// Lấy dữ liệu
String? token = service.token;
Map<String, dynamic>? user = service.user;
bool isReady = service.isReady;
String? error = service.error;
// Lưu dữ liệu
service.storeData(token, user);
// Xóa dữ liệu
service.clearData();
// Gọi method SDK trực tiếp
dynamic result = await service.callSDKMethod('getInfo', ['USER_NAME']);
// Lấy thông tin user theo key
dynamic userInfo = await service.getUserInfo('USER_EMAIL');
// Lấy token
String? token = await service.getToken();
// Lấy token bất đồng bộ
String? token = await service.getTokenAsync();
// Đóng app
await service.closeApp({'message': 'Done'});
// Kiểm tra SDK có sẵn
bool sdkAvailable = service.isSDKAvailable();
// Kiểm tra trạng thái
bool isReady = service.isInitialized;
String? cachedToken = service.cachedToken;
String? error = service.lastError;
```
## X-App-SDK Methods (từ Super App)
## Luồng hoạt động
### getToken()
Lấy token đăng nhập người dùng từ Super App.
```javascript
// Super App cung cấp method này
window.getToken().then(token => {
console.log('Token người dùng:', token);
}).catch(error => {
console.error('Lỗi lấy token:', error);
});
```
1. **Khởi tạo**: App khởi tạo x-app-sdk khi start
2. **Splash Screen**: Tự động gọi `getToken()` để lấy token
3. **Fallback**: Nếu SDK không có token, fallback về URL params
4. **Login**: Sử dụng token để đăng nhập
5. **Close App**: Khi cần đóng app, gọi `closeApp()`
### getInfo(key)
Lấy thông tin người dùng từ Super App theo key.
## Test
```javascript
// Super App cung cấp method này
// Lấy user ID
window.getInfo('USER_ID').then(info => {
console.log('ID người dùng:', info.data);
}).catch(error => {
console.error('Lỗi lấy thông tin:', error);
});
### Local Development
// Lấy thông tin khác
window.getInfo('USER_NAME').then(info => {
console.log('Tên người dùng:', info.data);
});
```bash
# Test với mock SDK
./scripts/test_x_app_sdk.sh
```
### Available Keys
- `'USER_ID'`: ID người dùng
- `'USER_NAME'`: Tên người dùng
- `'USER_EMAIL'`: Email người dùng
- `'USER_PHONE'`: Số điện thoại
## Fallback Methods
### Production
Nếu `x-app-sdk` không khả dụng, app sẽ thử các phương pháp fallback:
Không cần thay đổi gì. x-app-sdk sẽ tự động detect Super App environment và hoạt động đúng.
1. **URL Parameters**: `?token=xxx&user=yyy`
2. **localStorage**: `app_host_token`, `app_host_user`
**Lưu ý**: x-app-sdk được load từ npm package, tự động detect Super App environment.
## Debugging
Để debug, mở Developer Tools và kiểm tra:
## Troubleshooting
1. Console logs với prefix `🔍`, `✅`, `❌`, `⚠️`
2. `window.AppHostData` object
3. Network tab để xem việc load x-app-sdk
### SDK không khởi tạo được
## Lưu ý
- Kiểm tra console log: `❌ XAppSDKService: x-app-sdk not found in window`
- Đảm bảo Super App đã load x-app-sdk trước khi load mini app
- Chỉ hoạt động trên web platform
- Super App đã có sẵn `x-app-sdk` library
- Mini app gọi `window.getToken()``window.getInfo()` từ Super App
- Data được lưu trong localStorage để sử dụng lại
- App tự động listen cho updates từ Super App
- SDK với dữ liệu thật chỉ được trả ra khi chạy trên Super App
- Khi chạy trên web thường sẽ có data mẫu có cấu trúc tương tự data thật
### Token không lấy được
## Troubleshooting
- Kiểm tra console log: `❌ SplashScreen - Failed to get token from SDK`
- Fallback sẽ tự động sử dụng URL params
- Kiểm tra Super App có expose `getToken()` method không
### Lỗi thường gặp:
### CloseApp không hoạt động
1. **"x-app-sdk not available from Super App"**: Super App chưa expose methods
2. **"No data available from Super App"**: Không lấy được data từ Super App
3. **CORS errors**: Cần cấu hình CORS cho Super App
- Kiểm tra console log: `❌ XAppSDKService: closeApp method not found`
- Fallback sẽ tự động sử dụng `window.history.back()` hoặc `window.close()`
### Giải pháp:
## Files liên quan
1. Kiểm tra Super App đã expose `window.getToken()``window.getInfo()` chưa
2. Kiểm tra console logs để xem chi tiết lỗi
3. Sử dụng fallback methods nếu cần
4. Kiểm tra `webIsSDKAvailable()` để xác nhận SDK có sẵn
- `lib/web/x_app_sdk_service.dart` - Service chính
- `lib/web/web_helper_web.dart` - Web implementation
- `lib/web/web_helper_stub.dart` - Stub cho non-web platforms
- `lib/web/web_helper.dart` - Export file
- `lib/screen/splash/splash_screen_viewmodel.dart` - Tích hợp trong splash
- `lib/base/app_navigator.dart` - Sử dụng closeApp
- `lib/core/app_initializer.dart` - Khởi tạo SDK
- `web/index.html` - Mock SDK cho development
- `scripts/test_x_app_sdk.sh` - Script test
......@@ -7,11 +7,8 @@ echo "🔧 Exporting Development Web App..."
# Kill server cũ
lsof -i :8080 | awk 'NR>1 {print $2}' | xargs kill -9 2>/dev/null || true
# Chuyển sang dev environment
./scripts/switch_env.sh dev
# Export web app
./export_web.sh
# Export web app cho môi trường dev
./export_web.sh dev
# Chạy server với CORS như run_dev
echo "🚀 Starting exported web app with CORS..."
......@@ -105,4 +102,3 @@ echo "Press Ctrl+C to stop the server"
# Wait for user to stop
wait $SERVER_PID
#!/bin/bash
# Script để export Flutter web app thành HTML/CSS cho production
# Tạo package sẵn sàng để deploy cho bên web
# 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
echo "🚀 Exporting Flutter web app for production..."
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 PRO environment automatically
echo "🔧 Switching to PRO environment..."
./scripts/switch_env.sh prod
# 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..."
......@@ -19,8 +22,8 @@ flutter clean
rm -rf .dart_tool build
flutter pub get
# Install web dependencies for x-app-sdk
echo "📦 Installing web dependencies for x-app-sdk..."
# Install web dependencies
echo "📦 Installing web dependencies..."
cd web
if [ -f "package.json" ]; then
npm install
......@@ -35,7 +38,7 @@ fi
cd ..
# Tạo thư mục export
EXPORT_DIR="web_export_$(date +%Y%m%d_%H%M%S)"
EXPORT_DIR="web_export_${TARGET_ENV}_$(date +%Y%m%d_%H%M%S)"
echo "📁 Creating export directory: $EXPORT_DIR"
mkdir -p "$EXPORT_DIR"
......@@ -45,6 +48,8 @@ flutter build web --release --source-maps --dart-define=FLUTTER_WEB_USE_SKIA=fal
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/"
......@@ -130,17 +135,6 @@ Có thể override bằng query parameters: `?env=production`
Firebase config được load từ `firebase_options.dart`
Đảm bảo Firebase project được cấu hình đúng cho domain này.
## x-app-sdk Integration
App được tích hợp với x-app-sdk để giao tiếp với Super App:
- **Token retrieval**: Lấy token từ Super App
- **User info**: Lấy thông tin user từ Super App
- **Fallback methods**: URL parameters và localStorage
- **Retry mechanism**: Tối đa 3 lần retry
### Test x-app-sdk:
1. Mở `test_urls.html` để test với URL parameters
2. Kiểm tra console log để debug
3. Test với Super App environment
## CORS Configuration
App cần CORS headers để gọi API:
......@@ -154,7 +148,6 @@ Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Acc
- 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
- Nếu có lỗi x-app-sdk, kiểm tra Super App environment
EOF
# Tạo file .htaccess cho Apache
......@@ -320,11 +313,6 @@ EOF
echo "4. Hoặc sử dụng Node.js: npm install && npm start"
echo ""
echo "🔧 X-App-SDK Integration:"
echo "- Mini app đã được tích hợp x-app-sdk để lấy token và user info từ Super App"
echo "- Super App cần expose window.getToken() và window.getInfo() methods"
echo "- Retry mechanism: Tối đa 3 lần retry cho Super App data"
echo "- Fallback methods: URL parameters và localStorage"
echo "- Xem chi tiết trong X_APP_SDK_INTEGRATION.md"
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 &)"
......
......@@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart';
import 'package:mypoint_flutter_app/web/web_helper.dart';
import '../configs/constants.dart';
import '../preference/data_preference.dart';
import '../resources/base_color.dart';
......@@ -10,6 +9,7 @@ import '../shared/router_gage.dart';
import '../widgets/alert/custom_alert_dialog.dart';
import '../widgets/alert/data_alert_model.dart';
import '../widgets/alert/popup_data_model.dart';
import 'package:mypoint_flutter_app/web/web_helper.dart';
class AppNavigator {
static final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
......@@ -37,13 +37,13 @@ class AppNavigator {
text: "Đã hiểu",
onPressed: () {
_authDialogShown = false;
if (kIsWeb) {
webCloseApp({
'message': message.isNotEmpty ? message : description,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
return;
}
// if (kIsWeb) {
// webCloseApp({
// 'message': message.isNotEmpty ? message : description,
// 'timestamp': DateTime.now().millisecondsSinceEpoch,
// });
// return;
// }
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
if (phone.isNotEmpty) {
Get.offAllNamed(loginScreen, arguments: {'phone': phone});
......
import 'package:flutter/foundation.dart';
class UrlParams {
static String? _token;
static String? _userId;
static String? get token => _token;
static String? get userId => _userId;
static void setToken(String? token) => _token = token;
static void setUserId(String? userId) => _userId = userId;
static Map<String, String?> get allParams => {
'token': token,
'user_id': userId,
};
static bool get hasToken => token != null && token!.isNotEmpty;
static bool get hasUserId => userId != null && userId!.isNotEmpty;
// Helper method để lấy token cho API calls
static String? getTokenForApi() {
return token;
}
// Helper method để lấy userId cho API calls
static String? getUserIdForApi() {
return userId;
}
@override
String toString() => 'UrlParams: $allParams';
}
......@@ -10,8 +10,7 @@ import 'package:mypoint_flutter_app/firebase/push_notification.dart';
import 'package:mypoint_flutter_app/firebase/push_setup.dart';
import 'package:mypoint_flutter_app/base/app_loading.dart';
import 'package:mypoint_flutter_app/env_loader.dart';
import 'package:mypoint_flutter_app/web/web_app_initializer.dart';
import 'package:mypoint_flutter_app/core/deep_link_service.dart';
import 'package:mypoint_flutter_app/web/web_helper.dart';
/// Main app initialization and setup
class AppInitializer {
......@@ -30,13 +29,27 @@ class AppInitializer {
await _initializeFirebase();
// Fetch user point if logged in
await _fetchUserPointIfLoggedIn();
// Initialize web-specific features
await WebAppInitializer.initialize();
// Initialize deep links
await DeepLinkService().initialize();
// Initialize web-specific features (including x-app-sdk)
await _initializeWebFeatures();
print('✅ App initialization completed');
}
/// Initialize web-specific features
static Future<void> _initializeWebFeatures() async {
if (kIsWeb) {
print('🌐 Initializing web-specific features...');
try {
// Initialize x-app-sdk
await webInitializeXAppSDK();
print('✅ Web features initialized successfully');
} catch (e) {
print('❌ Error initializing web features: $e');
}
} else {
print('📱 Skipping web features initialization for mobile');
}
}
/// Initialize Firebase and FCM (mobile only)
static Future<void> _initializeFirebase() async {
if (!kIsWeb) {
......
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
......@@ -6,11 +7,15 @@ import 'package:mypoint_flutter_app/resources/base_color.dart';
import 'package:mypoint_flutter_app/screen/splash/splash_screen.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:mypoint_flutter_app/core/app_initializer.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (kIsWeb) {
setUrlStrategy(PathUrlStrategy());
}
// Initialize all app features
await AppInitializer.initialize();
// Run the app
......
......@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import '../../configs/api_paths.dart';
import '../../configs/constants.dart';
import '../../base/app_navigator.dart';
import '../../services/logout_service.dart';
import '../dio_http_service.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../services/token_refresh_service.dart';
......@@ -16,6 +17,7 @@ class AuthInterceptor extends Interceptor {
APIPaths.login,
APIPaths.refreshToken,
APIPaths.logout,
'assets',
];
@override
......@@ -24,9 +26,8 @@ class AuthInterceptor extends Interceptor {
if (_isTokenInvalid(data)) {
response.requestOptions.extra[_kAuthHandledKey] = true;
// Kiểm tra xem path này có cần skip refresh token không
if (_shouldSkipRefreshToken(response.requestOptions.path)) {
if (_shouldSkipRefreshToken(response.requestOptions.path) || response.requestOptions.method == 'GET') {
handler.reject(
DioException(
requestOptions: response.requestOptions
......@@ -64,7 +65,6 @@ class AuthInterceptor extends Interceptor {
if (alreadyHandled) return;
// Kiểm tra xem path này có cần skip refresh token không
if (_shouldSkipRefreshToken(err.requestOptions.path) &&
(statusCode == 401 || statusCode == 403 || _isTokenInvalid(data))) {
handler.next(err);
......@@ -88,10 +88,7 @@ class AuthInterceptor extends Interceptor {
/// Kiểm tra xem path này có cần skip refresh token không
bool _shouldSkipRefreshToken(String path) {
print('🔍 Checking if path should skip refresh token: $path');
if (path.isEmpty) return false;
print('🔍 Cleaned path: $path');
// Kiểm tra xem path có chứa bất kỳ pattern nào trong danh sách skip không
for (String skipPath in _skipRefreshTokenPaths) {
if (path.contains(skipPath)) {
print('🔍 Path "$path" matches skip pattern "$skipPath", skipping refresh token.');
......@@ -141,6 +138,7 @@ class AuthInterceptor extends Interceptor {
}
Future<void> _performLogout(dynamic data) async {
LogoutService.logout();
await DataPreference.instance.clearData();
String? message;
if (data is Map<String, dynamic>) {
......@@ -150,19 +148,4 @@ class AuthInterceptor extends Interceptor {
}
await AppNavigator.showAuthAlertAndGoLogin(message ?? ErrorCodes.tokenInvalidMessage);
}
/// Thêm path vào danh sách skip refresh token
static void addSkipPath(String path) {
if (!_skipRefreshTokenPaths.contains(path)) {
_skipRefreshTokenPaths.add(path);
}
}
/// Xóa path khỏi danh sách skip refresh token
static void removeSkipPath(String path) {
_skipRefreshTokenPaths.remove(path);
}
/// Lấy danh sách các path đang skip refresh token
static List<String> get skipPaths => List.unmodifiable(_skipRefreshTokenPaths);
}
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
/// Dio interceptor that logs request / response in JSON friendly format.
class LoggerInterceptor extends Interceptor {
// Configs
final bool prettyPrintJson = false; // Mặc định: JSON 1 dòng dễ copy/paste
final bool chunkLogging = true; // Chia nhỏ log theo block để tránh bị cắt
final int chunkSize = 800; // Kích thước mỗi block
LoggerInterceptor({
this.prettyJson = true,
this.chunkSize = 800,
this.enableChunking = true,
});
/// Pretty print JSON instead of single-line.
final bool prettyJson;
/// Maximum size of each log chunk when [enableChunking] is true.
final int chunkSize;
/// Split large logs into chunks to avoid truncation.
final bool enableChunking;
final Logger _logger = Logger(
printer: PrettyPrinter(
......@@ -20,101 +32,165 @@ class LoggerInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final uri = options.uri;
final buffer = StringBuffer();
buffer.writeln('🚀 ${options.method} $uri');
buffer.writeln('Headers: ${_formatHeaders(options.headers)}');
final buffer = StringBuffer()
..writeln('🚀 ${options.method} $uri')
..writeln('Headers: ${_maskHeaders(options.headers)}');
if (options.queryParameters.isNotEmpty) {
buffer.writeln('Query: ${_stringify(options.queryParameters)}');
buffer.writeln('Query: ${_asReadableJson(options.queryParameters)}');
}
_emit(Level.info, 'REQUEST', uri.toString(), buffer.toString());
_log(Level.info, 'REQUEST', uri.toString(), buffer.toString());
if (options.data != null) {
final bodyString = _stringify(options.data);
_emitJsonBlocks(Level.info, 'REQUEST JSON', uri.toString(), bodyString);
_logBody(Level.info, 'REQUEST BODY', uri.toString(), options.data);
}
final curl = _buildCurlCommand(options);
_log(Level.info, 'CURL', uri.toString(), curl);
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final uri = response.requestOptions.uri;
final statusCode = response.statusCode;
final buffer = StringBuffer();
buffer.writeln('✅ $statusCode ${response.requestOptions.method} $uri');
if (response.headers.map.isNotEmpty) {
buffer.writeln('Resp Headers: ${_stringify(response.headers.map)}');
}
_emit(Level.debug, 'RESPONSE', uri.toString(), buffer.toString());
// emit body in copy-friendly blocks
final bodyString = _stringify(response.data);
_emitJsonBlocks(Level.debug, 'RESPONSE JSON', uri.toString(), bodyString);
final statusCode = response.statusCode ?? 0;
final buffer = StringBuffer()
..writeln('✅ $statusCode ${response.requestOptions.method} $uri');
// if (response.headers.map.isNotEmpty) {
// buffer.writeln('Resp Headers: ${_asReadableJson(response.headers.map)}');
// }
_log(Level.debug, 'RESPONSE', uri.toString(), buffer.toString());
_logBody(Level.debug, 'RESPONSE BODY', uri.toString(), response.data);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final uri = err.requestOptions.uri;
final statusCode = err.response?.statusCode ?? 'Unknown';
final buffer = StringBuffer();
buffer.writeln('❌ $statusCode ${err.requestOptions.method} $uri');
buffer.writeln('Error: ${err.message}');
_emit(Level.error, 'ERROR', uri.toString(), buffer.toString());
final statusCode = err.response?.statusCode;
final buffer = StringBuffer()
..writeln('❌ ${statusCode ?? 'Unknown'} ${err.requestOptions.method} $uri')
..writeln('Error: ${err.message}');
_log(Level.error, 'ERROR', uri.toString(), buffer.toString());
if (err.response?.data != null) {
final bodyString = _stringify(err.response?.data);
_emitJsonBlocks(Level.error, 'ERROR JSON', uri.toString(), bodyString);
_logBody(Level.error, 'ERROR BODY', uri.toString(), err.response?.data);
}
handler.next(err);
}
String _formatHeaders(Map<String, dynamic> headers) {
final filtered = Map<String, dynamic>.from(headers);
// Hide sensitive headers
if (filtered.containsKey('Authorization')) {
filtered['Authorization'] = '***';
String _maskHeaders(Map<String, dynamic> headers) {
final sanitized = _sanitizeHeaders(headers);
return sanitized.toString();
}
return filtered.toString();
void _logBody(Level level, String phase, String uri, dynamic data) {
final body = _asReadableJson(data);
_log(level, phase, uri, body);
}
String _stringify(dynamic data) {
String _asReadableJson(dynamic data) {
if (data == null) return 'null';
if (data is String) return data;
dynamic resolved = data;
if (data is String) {
final trimmed = data.trim();
if (_looksLikeJson(trimmed)) {
resolved = _tryDecode(trimmed) ?? data;
} else {
return data;
}
}
try {
if (prettyPrintJson) {
final encoder = const JsonEncoder.withIndent(' ');
return encoder.convert(data);
if (resolved is String) {
return resolved;
}
return jsonEncode(data);
final encoder = prettyJson
? const JsonEncoder.withIndent(' ')
: const JsonEncoder();
return encoder.convert(resolved);
} catch (_) {
return data.toString();
return resolved.toString();
}
}
void _emit(Level level, String phase, String uri, String message) {
if (!chunkLogging || message.length <= chunkSize) {
dynamic _tryDecode(String text) {
try {
return jsonDecode(text);
} catch (_) {
return null;
}
}
bool _looksLikeJson(String text) {
return (text.startsWith('{') && text.endsWith('}')) ||
(text.startsWith('[') && text.endsWith(']'));
}
void _log(Level level, String phase, String uri, String message) {
if (!enableChunking || message.length <= chunkSize) {
_logger.log(level, '[$phase] $uri\n$message');
return;
}
final total = (message.length / chunkSize).ceil();
var index = 0;
for (var i = 0; i < message.length; i += chunkSize) {
final end = (i + chunkSize < message.length) ? i + chunkSize : message.length;
index += 1;
_logger.log(level, '[$phase PART $index/$total] $uri\n${message.substring(i, end)}');
_logger.log(
level,
'[$phase PART $index/$total] $uri\n${message.substring(i, end)}',
);
}
}
void _emitJsonBlocks(Level level, String phase, String uri, String jsonText) {
if (!chunkLogging || jsonText.length <= chunkSize) {
_logger.log(level, '[$phase FULL] $uri');
_logger.log(level, jsonText);
return;
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
final filtered = Map<String, dynamic>.from(headers);
const sensitive = {'authorization', 'cookie', 'set-cookie'};
filtered.updateAll((key, value) {
if (sensitive.contains(key.toLowerCase())) {
return '***';
}
final total = (jsonText.length / chunkSize).ceil();
var index = 0;
for (var i = 0; i < jsonText.length; i += chunkSize) {
final end = (i + chunkSize < jsonText.length) ? i + chunkSize : jsonText.length;
index += 1;
_logger.log(level, '[$phase PART $index/$total] $uri');
_logger.log(level, jsonText.substring(i, end));
return value;
});
return filtered;
}
String _buildCurlCommand(RequestOptions options) {
final sanitizedHeaders = _sanitizeHeaders(options.headers);
final buffer = StringBuffer('curl -X ${options.method}');
sanitizedHeaders.forEach((key, value) {
if (value == null) return;
buffer.write(" -H '${_escapeSingleQuotes('$key: $value')}'");
});
if (options.data != null) {
final payload = _payloadForCurl(options.data);
buffer.write(" --data '${_escapeSingleQuotes(payload)}'");
}
buffer.write(" '${options.uri}'");
return buffer.toString();
}
String _payloadForCurl(dynamic data) {
if (data == null) return '';
if (data is String) return data;
try {
return jsonEncode(data);
} catch (_) {
return data.toString();
}
}
String _escapeSingleQuotes(String value) => value.replaceAll("'", r"'\''");
}
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
......@@ -25,6 +27,16 @@ class RequestInterceptor extends Interceptor {
}
options.headers.addAll(headers);
// On web, ensure request payload is JSON encoded to avoid FormData fallback.
if (kIsWeb && options.data is Map) {
try {
options.data = jsonEncode(options.data);
} catch (_) {
// If encoding fails, keep original data.
}
}
handler.next(options);
}
}
......@@ -101,7 +101,6 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
var deviceKey = await DeviceInfo.getDeviceId();
var key = "$phone+_=$deviceKey/*8854";
final body = {"device_key": deviceKey, "phone_number": phone, "key": key.toSha256()};
print('body: $body');
return requestNormal(
APIPaths.checkPhoneNumber,
Method.POST,
......@@ -162,6 +161,17 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
return requestNormal(APIPaths.login, Method.POST, body, (data) => LoginTokenResponseModel.fromJson(data as Json));
}
Future<BaseResponseModel<EmptyCodable>> logout() async {
var deviceKey = await DeviceInfo.getDeviceId();
var phone = DataPreference.instance.phone ?? "";
final body = {
"username": phone,
"device_key": deviceKey,
"lang": "vi",
};
return requestNormal(APIPaths.login, Method.POST, body, (data) => EmptyCodable.fromJson(data as Json));
}
Future<BaseResponseModel<LoginTokenResponseModel>> loginWithBiometric(String phone) async {
var deviceKey = await DeviceInfo.getDeviceId();
var bioToken = await DataPreference.instance.getBioToken(phone) ?? "";
......@@ -381,7 +391,6 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
}
Future<BaseResponseModel<GameBundleItemModel>> getGameDetail(String id) async {
print("RestfulAPIClientAllRequest getGameDetail - id: $id");
final path = APIPaths.getGameDetail.replaceAll("%@", id);
return requestNormal(path, Method.POST, {}, (data) {
return GameBundleItemModel.fromJson(data as Json);
......@@ -1003,7 +1012,6 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
}
Future<BaseResponseModel<EmptyCodable>> pushNotificationDeviceUpdateToken(String token) async {
print("pushNotificationDeviceUpdateToken FCM: $token");
var deviceKey = await DeviceInfo.getDeviceId();
final details = await DeviceInfo.getDetails();
String? accessToken = DataPreference.instance.token ?? "";
......
......@@ -4,7 +4,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart';
import '../screen/popup_manager/popup_manager_viewmodel.dart';
import '../web/web_helper_stub.dart';
class DataPreference {
static final DataPreference _instance = DataPreference._internal();
......@@ -78,7 +77,6 @@ class DataPreference {
Future<void> clearLoginToken() async {
_loginToken = null;
webClearStorage();
final prefs = await SharedPreferences.getInstance();
await prefs.remove('login_token');
await PopupManagerViewModel.instance.reset();
......
......@@ -15,7 +15,6 @@ class UserPointManager extends RestfulApiViewModel {
int get point => _userPoint.value;
Future<int?> fetchUserPoint() async {
print("fetchUserPoint");
if (!DataPreference.instance.logged) return null;
try {
final response = await client.getHomeHeaderData();
......
......@@ -120,3 +120,4 @@ class HealthBookItem extends StatelessWidget {
}
}
......@@ -54,7 +54,15 @@ class _CampaignDetailScreenState extends BaseState<CampaignDetailScreen> with Ba
body: Obx(() {
CampaignDetailModel? pageDetail = _viewModel.campaignDetail.value.data?.pageDetail;
if (pageDetail == null) {
return const Center(child: EmptyWidget());
return Stack(
children: [
const Center(child: EmptyWidget()),
Positioned(top: MediaQuery
.of(context)
.padding
.top + 8, left: 8, child: CustomBackButton()),
],
);
}
final thumbnail = pageDetail.thumbnail ?? "";
final publishDate = pageDetail.publishDate ?? "";
......
......@@ -11,6 +11,7 @@ import '../../preference/package_info.dart';
import '../../preference/point/header_home_model.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart';
import '../../services/logout_service.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
......@@ -315,7 +316,8 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
onPressed: () async {
LogoutService.logout();
DataPreference.instance.clearLoginToken();
_safeBackToLogin();
},
......
Supports Markdown
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