Commit 0b973e61 authored by DatHV's avatar DatHV
Browse files

update build web, ui

parent b7cceccb
...@@ -11,13 +11,13 @@ class AchievementCarousel extends StatelessWidget { ...@@ -11,13 +11,13 @@ class AchievementCarousel extends StatelessWidget {
const AchievementCarousel({super.key, required this.items, this.sectionConfig, this.onTap}); const AchievementCarousel({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() { void _handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin(); sectionConfig?.buttonViewAll?.directionalScreen?.begin();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width/1.6;
if (items.isEmpty) return const SizedBox.shrink(); if (items.isEmpty) return const SizedBox.shrink();
return Column( return Column(
...@@ -28,7 +28,7 @@ class AchievementCarousel extends StatelessWidget { ...@@ -28,7 +28,7 @@ class AchievementCarousel extends StatelessWidget {
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null, onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
), ),
SizedBox( SizedBox(
height: width*180/230/1.6, height: width*180/230,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
...@@ -53,17 +53,21 @@ class AchievementCard extends StatelessWidget { ...@@ -53,17 +53,21 @@ class AchievementCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width/1.6;
final imageUrl = (achievement.images?.isNotEmpty == true) ? achievement.images?.first.imageUrl : ""; final imageUrl = (achievement.images?.isNotEmpty == true) ? achievement.images?.first.imageUrl : "";
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: ClipRRect( child: SizedBox(
borderRadius: BorderRadius.circular(12), width: widthItem,
child: loadNetworkImage( child: ClipRRect(
url: imageUrl, borderRadius: BorderRadius.circular(12),
fit: BoxFit.cover, child: loadNetworkImage(
width: 280, url: imageUrl,
height: 140, fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_logo.png', width: double.infinity,
height: widthItem*180/230,
placeholderAsset: 'assets/images/ic_logo.png',
),
), ),
), ),
); );
......
...@@ -10,7 +10,7 @@ class MyProductCarouselWidget extends StatelessWidget { ...@@ -10,7 +10,7 @@ class MyProductCarouselWidget extends StatelessWidget {
final void Function(MyProductModel)? onTap; final void Function(MyProductModel)? onTap;
const MyProductCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap}); const MyProductCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() { void _handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin(); sectionConfig?.buttonViewAll?.directionalScreen?.begin();
} }
......
...@@ -11,7 +11,7 @@ class NewsCarouselWidget extends StatelessWidget { ...@@ -11,7 +11,7 @@ class NewsCarouselWidget extends StatelessWidget {
final void Function(PageItemModel)? onTap; final void Function(PageItemModel)? onTap;
const NewsCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap}); const NewsCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() { void _handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin(); sectionConfig?.buttonViewAll?.directionalScreen?.begin();
} }
......
...@@ -7,7 +7,6 @@ import '../../networking/restful_api_viewmodel.dart'; ...@@ -7,7 +7,6 @@ import '../../networking/restful_api_viewmodel.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../widgets/custom_toast_message.dart'; import '../../widgets/custom_toast_message.dart';
import '../splash/splash_screen_viewmodel.dart';
import 'otp_viewmodel.dart'; import 'otp_viewmodel.dart';
class DeleteAccountOtpRepository extends RestfulApiViewModel implements IOtpRepository { class DeleteAccountOtpRepository extends RestfulApiViewModel implements IOtpRepository {
......
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../../base/base_response_model.dart';
import '../../splash/splash_screen_viewmodel.dart';
part 'create_otp_response_model.g.dart'; part 'create_otp_response_model.g.dart';
@JsonSerializable() @JsonSerializable()
......
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../splash/splash_screen_viewmodel.dart'; import '../../../base/base_response_model.dart';
import 'otp_claim_verify_response_model.dart'; import 'otp_claim_verify_response_model.dart';
part 'otp_verify_response_model.g.dart'; part 'otp_verify_response_model.g.dart';
......
...@@ -7,7 +7,6 @@ import '../../base/base_response_model.dart'; ...@@ -7,7 +7,6 @@ import '../../base/base_response_model.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../create_pass/create_pass_screen.dart'; import '../create_pass/create_pass_screen.dart';
import '../create_pass/signup_create_password_repository.dart'; import '../create_pass/signup_create_password_repository.dart';
import '../login/login_screen.dart';
import 'model/otp_verify_response_model.dart'; import 'model/otp_verify_response_model.dart';
import 'otp_viewmodel.dart'; import 'otp_viewmodel.dart';
......
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:mypoint_flutter_app/directional/directional_screen.dart'; import 'package:mypoint_flutter_app/directional/directional_screen.dart';
...@@ -195,16 +196,14 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po ...@@ -195,16 +196,14 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
} }
Widget _buildMenuItems() { Widget _buildMenuItems() {
final menuItems = [ var menuItems = [
{ {
'icon': Icons.monetization_on_outlined, 'icon': Icons.monetization_on_outlined,
'assets': 'assets/images/ic_point.png', 'assets': 'assets/images/ic_point.png',
'title': 'Săn điểm', 'title': 'Săn điểm',
'type': 'APP_SCREEN_POINT_HUNTING', 'type': 'APP_SCREEN_POINT_HUNTING',
}, },
{'icon': Icons.qr_code_2, 'title': 'QR Code', 'type': 'APP_SCREEN_QR_CODE'},
{'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': 'DAILY_CHECKIN'}, {'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': 'DAILY_CHECKIN'},
{'icon': Icons.border_right, 'title': 'Hoá đơn điện', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'},
// {'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'}, // {'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'},
{'icon': Icons.gif_box_outlined, 'title': 'Ưu đãi của tôi', 'type': 'APP_SCREEN_MY_PURCHASE_ITEMS'}, {'icon': Icons.gif_box_outlined, 'title': 'Ưu đãi của tôi', 'type': 'APP_SCREEN_MY_PURCHASE_ITEMS'},
{ {
...@@ -229,6 +228,13 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po ...@@ -229,6 +228,13 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
{'icon': Icons.logout, 'title': 'Đăng xuất', 'color': Colors.red[400], 'type': 'LOGOUT'}, {'icon': Icons.logout, 'title': 'Đăng xuất', 'color': Colors.red[400], 'type': 'LOGOUT'},
]; ];
if (!kIsWeb) {
menuItems.insertAll(2, [
{'icon': Icons.qr_code_2, 'title': 'QR Code', 'type': 'APP_SCREEN_QR_CODE',},
{'icon': Icons.border_right, 'title': 'Hoá đơn điện', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC',},
]
);
}
return Container( return Container(
color: Colors.white, color: Colors.white,
child: Column( child: Column(
......
// setting_screen.dart // setting_screen.dart
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:mypoint_flutter_app/screen/setting/setting_viewmodel.dart'; import 'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart';
...@@ -57,25 +58,26 @@ class _SettingScreenState extends State<SettingScreen> { ...@@ -57,25 +58,26 @@ class _SettingScreenState extends State<SettingScreen> {
onTap: () => Get.to(ChangePassScreen()), onTap: () => Get.to(ChangePassScreen()),
), ),
_buildDivider(), _buildDivider(),
_buildSettingItem( if (!kIsWeb)
icon: Icons.fingerprint, _buildSettingItem(
title: 'Xác thực sinh trắc học', icon: Icons.fingerprint,
showTrailing: false, title: 'Xác thực sinh trắc học',
trailing: Switch( showTrailing: false,
value: viewModel.biometricEnabled, trailing: Switch(
onChanged: (value) async { value: viewModel.biometricEnabled,
final result = await viewModel.toggleBiometric(value); onChanged: (value) async {
setState(() { final result = await viewModel.toggleBiometric(value);
viewModel.biometricEnabled = result; setState(() {
}); viewModel.biometricEnabled = result;
}, });
activeColor: Colors.white, },
activeTrackColor: Colors.green, activeColor: Colors.white,
inactiveThumbColor: Colors.white, activeTrackColor: Colors.green,
inactiveTrackColor: Colors.grey.shade400, inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey.shade400,
),
onTap: () {},
), ),
onTap: () {},
),
_buildDivider(), _buildDivider(),
_buildSettingItem( _buildSettingItem(
icon: Icons.devices_other, icon: Icons.devices_other,
...@@ -85,18 +87,19 @@ class _SettingScreenState extends State<SettingScreen> { ...@@ -85,18 +87,19 @@ class _SettingScreenState extends State<SettingScreen> {
}, },
), ),
_buildDivider(), _buildDivider(),
_buildSettingItem( if (!kIsWeb)
icon: Icons.delete_outline, _buildSettingItem(
title: 'Xóa tài khoản', icon: Icons.delete_outline,
onTap: () { title: 'Xóa tài khoản',
BottomSheetHelper.showBottomSheetPopup( onTap: () {
child: const DeleteAccountDialog(), BottomSheetHelper.showBottomSheetPopup(
); child: const DeleteAccountDialog(),
}, );
textColor: Colors.red, },
iconColor: Colors.red, textColor: Colors.red,
showTrailing: false, iconColor: Colors.red,
), showTrailing: false,
),
], ],
), ),
), ),
......
...@@ -29,7 +29,7 @@ class _SplashScreenState extends BaseState<SplashScreen> with BasicState { ...@@ -29,7 +29,7 @@ class _SplashScreenState extends BaseState<SplashScreen> with BasicState {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final list = response.data?.updateRequest; final list = response.data?.updateRequest;
if (list == null || list.isEmpty) { if (list == null || list.isEmpty) {
_viewModel.getUserProfile(); _viewModel.makeDataFollowInitApp();
return; return;
} }
var result = response?.data?.updateRequest?.first; var result = response?.data?.updateRequest?.first;
...@@ -103,7 +103,7 @@ class _SplashScreenState extends BaseState<SplashScreen> with BasicState { ...@@ -103,7 +103,7 @@ class _SplashScreenState extends BaseState<SplashScreen> with BasicState {
text: "Để sau", text: "Để sau",
onPressed: () { onPressed: () {
Get.back(); Get.back();
_viewModel.getUserProfile(); _viewModel.makeDataFollowInitApp();
}, },
bgColor: Colors.white, bgColor: Colors.white,
textColor: BaseColor.primary500, textColor: BaseColor.primary500,
......
...@@ -8,6 +8,7 @@ import '../../base/base_response_model.dart'; ...@@ -8,6 +8,7 @@ import '../../base/base_response_model.dart';
import '../../configs/url_params.dart'; import '../../configs/url_params.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 '../../web/web_helper_stub.dart';
import 'models/update_response_model.dart'; import 'models/update_response_model.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../preference/point/point_manager.dart'; import '../../preference/point/point_manager.dart';
...@@ -37,23 +38,19 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -37,23 +38,19 @@ class SplashScreenViewModel extends RestfulApiViewModel {
} }
} }
Future<void> getUserProfile() async { Future<void> makeDataFollowInitApp() async {
// Không cần loadFromStorage nữa vì token đã được set từ main.dart
// await UrlParams.loadFromStorage();
if (DataPreference.instance.logged) { if (DataPreference.instance.logged) {
_freshUserProfile(); _freshUserProfile();
return; return;
} }
final token = UrlParams.getTokenForApi() ?? ""; final tokenFormWeb = UrlParams.getTokenForApi() ?? "";
print('🔍 SplashScreen - Token from URL: $token'); print('🔍 SplashScreen - Token from URL: $tokenFormWeb');
if (!kIsWeb || token.isEmpty) { if (tokenFormWeb.isEmpty) {
print('❌ No token found, going to onboarding'); _directionWhenTokenInvalid();
Get.toNamed(onboardingScreen);
return; return;
} }
print('✅ Token found, proceeding with login'); print('✅ Token found, proceeding with login');
LoginTokenResponseModel tokenModel = LoginTokenResponseModel(accessToken: token); LoginTokenResponseModel tokenModel = LoginTokenResponseModel(accessToken: tokenFormWeb);
await DataPreference.instance.saveLoginToken(tokenModel); await DataPreference.instance.saveLoginToken(tokenModel);
_freshUserProfile(); _freshUserProfile();
return; return;
...@@ -68,10 +65,23 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -68,10 +65,23 @@ class SplashScreenViewModel extends RestfulApiViewModel {
_freshDataAndToMainScreen(userProfile); _freshDataAndToMainScreen(userProfile);
} else { } else {
DataPreference.instance.clearLoginToken(); DataPreference.instance.clearLoginToken();
Get.toNamed(onboardingScreen); _directionWhenTokenInvalid();
} }
} }
void _directionWhenTokenInvalid() {
Get.toNamed(onboardingScreen);
return;
if (kIsWeb) {
print('❌ No token found on web, cannot proceed');
webCloseApp({
'message': 'No token found, cannot proceed',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
Get.toNamed(onboardingScreen);
}
}
void _freshDataAndToMainScreen(ProfileResponseModel userProfile) async { void _freshDataAndToMainScreen(ProfileResponseModel userProfile) async {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await DataPreference.instance.saveUserProfile(userProfile); await DataPreference.instance.saveUserProfile(userProfile);
...@@ -80,12 +90,4 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -80,12 +90,4 @@ class SplashScreenViewModel extends RestfulApiViewModel {
Get.toNamed(mainScreen); Get.toNamed(mainScreen);
}); });
} }
} }
\ No newline at end of file
class EmptyCodable {
EmptyCodable.fromJson(dynamic json);
Map<String, dynamic> toJson() {
return {};
}
}
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_core/src/get_main.dart';
...@@ -41,13 +43,15 @@ class VoucherListItem extends StatelessWidget { ...@@ -41,13 +43,15 @@ class VoucherListItem extends StatelessWidget {
final brandName = product.brand?.name ?? ''; final brandName = product.brand?.name ?? '';
final brandLogo = product.brand?.logo ?? ''; final brandLogo = product.brand?.logo ?? '';
final String? bgImage = product.banner?.url; final String? bgImage = product.banner?.url;
final widthImage = MediaQuery.of(context).size.width/2-32;
final heightImage = widthImage*9/16;
return Column( return Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SizedBox( child: SizedBox(
height: 112, height: max(heightImage, 112),
child: Row( child: Row(
children: [ children: [
ClipRRect( ClipRRect(
......
...@@ -97,6 +97,19 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba ...@@ -97,6 +97,19 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba
return; return;
} }
input = args; input = args;
// Web platform: mở URL trong tab mới và đóng màn hình ngay
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _openUrlInBrowser();
if (mounted) {
Get.back();
}
});
return;
}
// Mobile platform: khởi tạo WebView
_controller = _controller =
WebViewController() WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
...@@ -120,6 +133,31 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba ...@@ -120,6 +133,31 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba
@override @override
Widget createBody() { Widget createBody() {
// Web platform: hiển thị loading hoặc empty container
if (kIsWeb) {
return Scaffold(
appBar: CustomNavigationBar(
title: "Thanh toán",
leftButtons: [
CustomBackButton(
onPressed: () => Get.back(),
),
],
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Đang mở trang thanh toán...'),
],
),
),
);
}
// Mobile platform: hiển thị WebView
return Scaffold( return Scaffold(
appBar: CustomNavigationBar( appBar: CustomNavigationBar(
title: "Thanh toán", title: "Thanh toán",
...@@ -189,6 +227,28 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba ...@@ -189,6 +227,28 @@ class _PaymentWebViewScreenState extends BaseState<PaymentWebViewScreen> with Ba
Get.snackbar('Thông báo', 'Đi tới danh sách hợp đồng điện'); // placeholder Get.snackbar('Thông báo', 'Đi tới danh sách hợp đồng điện'); // placeholder
} }
/// Mở URL trong browser (web platform)
Future<void> _openUrlInBrowser() async {
try {
final uri = Uri.parse(input.url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
debugPrint('❌ Error opening URL: $e');
// Fallback: mở trong tab hiện tại
try {
await launchUrl(
Uri.parse(input.url),
mode: LaunchMode.platformDefault,
);
} catch (e2) {
debugPrint('❌ Error opening URL (fallback): $e2');
}
}
}
_onBackPressed() { _onBackPressed() {
final dataAlert = DataAlertModel( final dataAlert = DataAlertModel(
title: "Huỷ đơn hàng?", title: "Huỷ đơn hàng?",
......
import 'package:flutter/foundation.dart';
import 'web_helper.dart';
/// Example usage of closeApp functionality
class CloseAppExample {
/// Close app without returning any data
static void closeAppSimple() {
if (kIsWeb) {
print('🚪 Closing app (simple)...');
webCloseApp();
} else {
print('⚠️ closeApp only works on web platform');
}
}
/// Close app with success data
static void closeAppWithSuccess() {
if (kIsWeb) {
print('🚪 Closing app with success data...');
webCloseApp({
'result': 'success',
'message': 'Operation completed successfully',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
print('⚠️ closeApp only works on web platform');
}
}
/// Close app with error data
static void closeAppWithError(String errorMessage) {
if (kIsWeb) {
print('🚪 Closing app with error data...');
webCloseApp({
'result': 'error',
'message': errorMessage,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
print('⚠️ closeApp only works on web platform');
}
}
/// Close app with custom data
static void closeAppWithCustomData(Map<String, dynamic> customData) {
if (kIsWeb) {
print('🚪 Closing app with custom data...');
webCloseApp({
'result': 'custom',
'data': customData,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
print('⚠️ closeApp only works on web platform');
}
}
/// Example: Close app after successful payment
static void closeAppAfterPayment({
required String transactionId,
required double amount,
required String currency,
}) {
if (kIsWeb) {
print('🚪 Closing app after payment...');
webCloseApp({
'result': 'payment_success',
'transactionId': transactionId,
'amount': amount,
'currency': currency,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
print('⚠️ closeApp only works on web platform');
}
}
/// Example: Close app after form submission
static void closeAppAfterFormSubmission({
required String formType,
required Map<String, dynamic> formData,
}) {
if (kIsWeb) {
print('🚪 Closing app after form submission...');
webCloseApp({
'result': 'form_submitted',
'formType': formType,
'formData': formData,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
} else {
print('⚠️ closeApp only works on web platform');
}
}
}
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/configs/url_params.dart';
import 'web_helper.dart';
/// Web-specific initialization and configuration
class WebAppInitializer {
static final WebAppInitializer _instance = WebAppInitializer._internal();
factory WebAppInitializer() => _instance;
WebAppInitializer._internal();
// Ensure web initialization runs only once per app lifecycle
static bool _didInit = false;
// Ensure we only start one polling sequence for SDK data
static bool _startedSdkPolling = false;
/// Initialize all web-specific features
static Future<void> initialize() async {
if (!kIsWeb) return;
if (_didInit) {
// Prevent re-initialization on hot reload / route changes
return;
}
_didInit = true;
print('🌐 Initializing web-specific features...');
// Handle URL parameters
_handleWebUrlParams();
// Initialize x-app-sdk
_initializeXAppSDK();
}
/// Handle URL parameters for web
static void _handleWebUrlParams() {
print('🔍 Handling web URL parameters...');
final uri = Uri.base;
print('🔍 Current URI: ${uri.toString()}');
final token = uri.queryParameters['token'];
final userId = uri.queryParameters['userId'];
print('🔍 Web URL Params: {token: $token, user_id: $userId}');
if (token != null && token.isNotEmpty) {
UrlParams.setToken(token);
UrlParams.setUserId(userId);
print('✅ Token set from URL: $token');
print('🔍 UrlParams after set: ${UrlParams.allParams}');
// Clean URL to remove query params
webReplaceUrl('/');
} else {
print('❌ No token found in URL parameters');
}
}
/// Initialize x-app-sdk service
static void _initializeXAppSDK() {
print('🔍 Initializing x-app-sdk...');
// Check if x-app-sdk is available from Super App
final isSDKAvailable = webIsSDKAvailable();
print('🔍 XAppSDK available from Super App: $isSDKAvailable');
// Always try to initialize once (no-op on non-web/stub)
webInitializeXAppSDK().then((_) {
print('✅ XAppSDK service initialized');
// Only poll for data if SDK is actually available from host
if (isSDKAvailable) {
if (_startedSdkPolling) return;
_startedSdkPolling = true;
// Wait a bit for JavaScript to initialize and then check for data
_checkForAppHostData(0); // Start with retry count 0
} else {
print('ℹ️ XAppSDK not available – skipping polling outside Super App.');
print('💡 Tip: Test with URL params, e.g. ?token=test123&userId=user123');
}
}).catchError((error) {
print('❌ Error initializing XAppSDK: $error');
});
}
/// Check for app host data with retry mechanism (max 3 retries)
static void _checkForAppHostData(int retryCount) {
print('🔍 Checking for app host data... (attempt ${retryCount + 1}/4)');
// Wait a bit for JavaScript to initialize
Future.delayed(const Duration(milliseconds: 1000), () {
final token = webGetAppHostToken();
final user = webGetAppHostUser();
final error = webGetAppHostError();
final isReady = webIsAppHostDataReady();
print('🔍 Data check result:');
print(' Token: ${token != null ? '***${token.substring(0, 8)}...' : 'null'}');
print(' User: ${user?.toString() ?? 'null'}');
print(' Error: ${error ?? 'null'}');
print(' Ready: $isReady');
if (token != null && token.isNotEmpty) {
print(' Token from Super App: ${token.substring(0, 8)}...');
UrlParams.setToken(token);
if (user != null) {
print(' User from Super App: $user');
UrlParams.setUserId(user['id']?.toString());
// Store user data for later use
webStoreAppHostData(token, user);
}
} else if (error != null) {
print(' Error from Super App: $error');
} else if (!isReady && retryCount < 3) {
print(' App host data not ready yet, will retry... (${retryCount + 1}/3)');
// Retry after a longer delay
Future.delayed(const Duration(milliseconds: 2000), () {
_checkForAppHostData(retryCount + 1);
});
} else if (retryCount >= 3) {
print(' Max retries reached (3), giving up on Super App data');
print('💡 You can test with URL parameters: ?token=test123&user={"id":"user123"}');
} else {
print(' No token found from Super App');
}
});
}
}
...@@ -2,3 +2,5 @@ export 'web_helper_stub.dart' ...@@ -2,3 +2,5 @@ export 'web_helper_stub.dart'
if (dart.library.html) 'web_helper_web.dart'; if (dart.library.html) 'web_helper_web.dart';
...@@ -8,4 +8,69 @@ void webClearStorage() { ...@@ -8,4 +8,69 @@ void webClearStorage() {
// no-op on non-web // no-op on non-web
} }
/// Get token from app host via x-app-sdk
String? webGetAppHostToken() {
return null;
}
/// Get user info from app host via x-app-sdk
Map<String, dynamic>? webGetAppHostUser() {
return null;
}
/// Check if app host data is ready
bool webIsAppHostDataReady() {
return false;
}
/// Get error message from app host
String? webGetAppHostError() {
return null;
}
/// Initialize x-app-sdk service
Future<void> webInitializeXAppSDK() async {
// no-op on non-web
}
/// Store app host data
void webStoreAppHostData(String token, Map<String, dynamic>? user) {
// no-op on non-web
}
/// Clear app host data
void webClearAppHostData() {
// no-op on non-web
}
/// Execute JavaScript in the web context
Future<dynamic> webExecuteJavaScript(String script) async {
return null;
}
/// Call x-app-sdk method if available
Future<dynamic> webCallXAppSDKMethod(String methodName, [List<dynamic>? args]) async {
return null;
}
/// Get user info by key from app host
Future<dynamic> webGetUserInfoByKey(String key) async {
return null;
}
/// Get token asynchronously from app host
Future<String?> webGetTokenAsync() async {
return null;
}
/// Check if x-app-sdk is available from Super App
bool webIsSDKAvailable() {
return false;
}
/// Close app and return to Super App
void webCloseApp([Map<String, dynamic>? data]) {
// no-op on non-web
}
// Web-specific implementations // Web-specific implementations
// ignore: avoid_web_libraries_in_flutter // ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html; import 'dart:convert';
import 'package:universal_html/html.dart' as html;
import 'x_app_sdk_service.dart';
void webReplaceUrl(String path) { void webReplaceUrl(String path) {
try { try {
...@@ -18,4 +20,134 @@ void webClearStorage() { ...@@ -18,4 +20,134 @@ void webClearStorage() {
} catch (_) {} } catch (_) {}
} }
/// Get token from app host via x-app-sdk
String? webGetAppHostToken() {
try {
return XAppSDKService().getToken();
} catch (e) {
print('❌ Error getting app host token: $e');
return null;
}
}
/// Get user info from app host via x-app-sdk
Map<String, dynamic>? webGetAppHostUser() {
try {
return XAppSDKService().getUser();
} catch (e) {
print('❌ Error getting app host user: $e');
return null;
}
}
/// Check if app host data is ready
bool webIsAppHostDataReady() {
try {
return XAppSDKService().isServiceReady;
} catch (e) {
print('❌ Error checking app host data ready: $e');
return false;
}
}
/// Get error message from app host
String? webGetAppHostError() {
try {
return XAppSDKService().getErrorMessage();
} catch (e) {
print('❌ Error getting app host error: $e');
return null;
}
}
/// Initialize x-app-sdk service
Future<void> webInitializeXAppSDK() async {
try {
await XAppSDKService().initialize();
XAppSDKService().listenForUpdates();
} catch (e) {
print('❌ Error initializing x-app-sdk: $e');
}
}
/// Store app host data
void webStoreAppHostData(String token, Map<String, dynamic>? user) {
try {
XAppSDKService().storeData(token, user);
} catch (e) {
print('❌ Error storing app host data: $e');
}
}
/// Clear app host data
void webClearAppHostData() {
try {
XAppSDKService().clearData();
} catch (e) {
print('❌ Error clearing app host data: $e');
}
}
/// Execute JavaScript in the web context
Future<dynamic> webExecuteJavaScript(String script) async {
try {
// For now, we'll use a simpler approach
// This method is mainly for future extensibility
print('⚠️ webExecuteJavaScript is not fully implemented yet');
return null;
} catch (e) {
print('❌ Error executing JavaScript: $e');
return null;
}
}
/// Call x-app-sdk method if available
Future<dynamic> webCallXAppSDKMethod(String methodName, [List<dynamic>? args]) async {
try {
return await XAppSDKService().callSDKMethod(methodName, args);
} catch (e) {
print('❌ Error calling x-app-sdk method $methodName: $e');
return null;
}
}
/// Get user info by key from app host
Future<dynamic> webGetUserInfoByKey(String key) async {
try {
return await XAppSDKService().getUserInfo(key);
} catch (e) {
print('❌ Error getting user info by key $key: $e');
return null;
}
}
/// Get token asynchronously from app host
Future<String?> webGetTokenAsync() async {
try {
return await XAppSDKService().getTokenAsync();
} catch (e) {
print('❌ Error getting token async: $e');
return null;
}
}
/// Check if x-app-sdk is available from Super App
bool webIsSDKAvailable() {
try {
return XAppSDKService().isSDKAvailable();
} catch (e) {
print('❌ Error checking SDK availability: $e');
return false;
}
}
/// Close app and return to Super App
void webCloseApp([Map<String, dynamic>? data]) {
try {
XAppSDKService().closeApp(data);
} catch (e) {
print('❌ Error closing app: $e');
}
}
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:universal_html/html.dart' as html;
import 'package:universal_html/js_util.dart';
class XAppSDKService {
static final XAppSDKService _instance = XAppSDKService._internal();
factory XAppSDKService() => _instance;
XAppSDKService._internal();
String? _token;
Map<String, dynamic>? _user;
bool _isReady = false;
String? _error;
String? get token => _token;
Map<String, dynamic>? get user => _user;
bool get isReady => _isReady;
String? get error => _error;
/// Initialize x-app-sdk service and get data from app host
Future<void> initialize() async {
if (!kIsWeb) return;
try {
// Wait longer for the JavaScript to initialize
await Future.delayed(const Duration(milliseconds: 1000));
// Check if AppHostData is available in window
final appHostData = getProperty(html.window, 'AppHostData');
if (appHostData != null) {
final data = jsonDecode(appHostData.toString());
_token = data['token'];
_user = data['user'] != null ? Map<String, dynamic>.from(data['user']) : null;
_isReady = data['isReady'] ?? false;
_error = data['error'];
print('✅ XAppSDK Service initialized:');
print(' Token: ${_token != null ? '***${_token!.substring(_token!.length - 4)}' : 'null'}');
print(' User: ${_user?.toString() ?? 'null'}');
print(' Ready: $_isReady');
if (_error != null) {
print(' Error: $_error');
}
} else {
print(' AppHostData not found in window, trying fallback...');
await _tryFallbackMethod();
}
} catch (e) {
print(' Error initializing XAppSDK Service: $e');
await _tryFallbackMethod();
}
}
/// Fallback method to get data from URL parameters or localStorage
Future<void> _tryFallbackMethod() async {
try {
// Try to get from URL parameters first
final uri = Uri.base;
final token = uri.queryParameters['token'];
final userStr = uri.queryParameters['user'];
if (token != null && token.isNotEmpty) {
_token = token;
if (userStr != null && userStr.isNotEmpty) {
try {
_user = jsonDecode(userStr);
} catch (e) {
print(' Failed to parse user from URL: $e');
}
}
_isReady = true;
print(' Data loaded from URL parameters (fallback)');
return;
}
// Try to get from localStorage
final storedToken = html.window.localStorage['app_host_token'];
final storedUser = html.window.localStorage['app_host_user'];
if (storedToken != null && storedToken.isNotEmpty) {
_token = storedToken;
if (storedUser != null && storedUser.isNotEmpty) {
try {
_user = jsonDecode(storedUser);
} catch (e) {
print(' Failed to parse user from localStorage: $e');
}
}
_isReady = true;
print(' Data loaded from localStorage (fallback)');
} else {
print(' No data found in URL parameters or localStorage');
_error = 'No data available from app host';
}
} catch (e) {
print(' Error in fallback method: $e');
_error = e.toString();
}
}
/// Get token from app host
String? getToken() {
return _token;
}
/// Get user info from app host
Map<String, dynamic>? getUser() {
return _user;
}
/// Check if service is ready
bool get isServiceReady => _isReady;
/// Get error message if any
String? getErrorMessage() {
return _error;
}
/// Clear stored data
void clearData() {
_token = null;
_user = null;
_isReady = false;
_error = null;
if (kIsWeb) {
try {
html.window.localStorage.remove('app_host_token');
html.window.localStorage.remove('app_host_user');
} catch (e) {
print(' Error clearing localStorage: $e');
}
}
}
/// Store data for future use
void storeData(String token, Map<String, dynamic>? user) {
_token = token;
_user = user;
_isReady = true;
_error = null;
if (kIsWeb) {
try {
html.window.localStorage['app_host_token'] = token;
if (user != null) {
html.window.localStorage['app_host_user'] = jsonEncode(user);
}
} catch (e) {
print(' Error storing data: $e');
}
}
}
/// Listen for data updates from app host
void listenForUpdates() {
if (!kIsWeb) return;
try {
// Set up a periodic check for updates using js_util
final intervalId = callMethod(html.window, 'setInterval', [
allowInterop(() {
final appHostData = getProperty(html.window, 'AppHostData');
if (appHostData != null) {
final data = jsonDecode(appHostData.toString());
final newToken = data['token'];
final newUser = data['user'];
final newReady = data['isReady'] ?? false;
final newError = data['error'];
if (newReady && (newToken != _token || newUser != _user)) {
_token = newToken;
_user = newUser != null ? Map<String, dynamic>.from(newUser) : null;
_isReady = newReady;
_error = newError;
print('🔄 XAppSDK data updated from app host');
}
}
}),
2000 // Check every 2 seconds
]);
// Store interval ID for potential cleanup
print(' Update listener set up with interval ID: $intervalId');
} catch (e) {
print(' Error setting up update listener: $e');
}
}
/// Call x-app-sdk method directly from Super App
Future<dynamic> callSDKMethod(String methodName, [List<dynamic>? args]) async {
if (!kIsWeb) return null;
try {
// Check if method is available from Super App
final methodExists = getProperty(html.window, methodName);
if (methodExists == null) {
print(' Method $methodName not available from Super App');
return null;
}
// Call method directly using callMethod
if (methodName == 'getToken') {
return await promiseToFuture(callMethod(html.window, 'getToken', []));
} else if (methodName == 'getInfo' && args != null && args.isNotEmpty) {
return await promiseToFuture(callMethod(html.window, 'getInfo', [args[0]]));
} else {
print(' Unsupported method: $methodName');
return null;
}
} catch (e) {
print(' Error calling SDK method $methodName: $e');
return null;
}
}
/// Get user info by key from Super App
Future<dynamic> getUserInfo(String key) async {
return await callSDKMethod('getInfo', [key]);
}
/// Get token from Super App
Future<String?> getTokenAsync() async {
return await callSDKMethod('getToken');
}
/// Check if x-app-sdk is available from Super App
bool isSDKAvailable() {
if (!kIsWeb) return false;
try {
final getToken = getProperty(html.window, 'getToken');
final getInfo = getProperty(html.window, 'getInfo');
return getToken != null && getInfo != null;
} catch (e) {
return false;
}
}
/// Close app and return to Super App
void closeApp([Map<String, dynamic>? data]) {
if (!kIsWeb) return;
try {
print('🚪 Closing app and returning to Super App...');
if (data != null) {
print('📤 Data to return: $data');
}
// Call the JavaScript closeApp function
callMethod(html.window, 'closeApp', [data]);
print(' closeApp called successfully');
} catch (e) {
print(' Error calling closeApp: $e');
// Fallback: try to close the window
try {
html.window.close();
} catch (fallbackError) {
print(' Fallback close also failed: $fallbackError');
}
}
}
}
#!/bin/bash
# Script để chạy development
echo "🔧 Running Development..."
# 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
# Chạy web app (sẽ tự mở Chrome)
./scripts/run_web_complete.sh
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