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