Commit 4bd580fa authored by DatHV's avatar DatHV
Browse files

update game card detail, mobile card

parent b75a9279
......@@ -7,7 +7,8 @@
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
......
......@@ -2,6 +2,7 @@
FLAVOR=dev
BASE_URL=https://api.sandbox.mypoint.com.vn/8854/gup2start/rest
LIB_TOKEN=dev_token_here
APP_DISPLAY_NAME = MyPoint DEV
// Inherit base Flutter settings
#include "Debug.xcconfig"
......
......@@ -2,6 +2,7 @@
FLAVOR=pro
BASE_URL=https://api.mypoint.com.vn/8854/gup2start/rest
LIB_TOKEN=prod_token_here
APP_DISPLAY_NAME = MyPoint
// Inherit base Flutter settings
#include "Release.xcconfig"
......
......@@ -2,7 +2,7 @@
FLAVOR=stg
BASE_URL=https://api.sandbox.mypoint.com.vn/8854/gup2start/rest
LIB_TOKEN=stg_token_here
APP_DISPLAY_NAME = MyPoint STG
// Inherit base Flutter settings
#include "Debug.xcconfig"
......
......@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
......@@ -54,6 +55,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
......
......@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
......
......@@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../configs/constants.dart';
import '../configs/constants.dart';
class AppLoading {
// Singleton ẩn
......@@ -77,7 +78,7 @@ class AppLoading {
fit: StackFit.expand,
children: [
if (barrierColor != null)
const SizedBox.expand( // không dùng Positioned
const SizedBox.expand(
child: IgnorePointer(ignoring: true, child: SizedBox()),
),
if (barrierColor != null)
......
......@@ -13,16 +13,148 @@ abstract class BaseScreen extends StatefulWidget {
const BaseScreen({super.key});
}
abstract class BaseState<Screen extends BaseScreen> extends State<Screen> {
abstract class BaseState<Screen extends BaseScreen> extends State<Screen> with WidgetsBindingObserver {
bool _isVisible = false;
bool _isPaused = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (kDebugMode) {
print("_show: $runtimeType");
}
// Gọi onInit sau khi initState
WidgetsBinding.instance.addPostFrameCallback((_) {
onInit();
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
onDestroy();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
if (_isPaused) {
_isPaused = false;
onAppResumed();
if (_isVisible) {
onStart();
}
}
break;
case AppLifecycleState.paused:
if (!_isPaused) {
_isPaused = true;
onAppPaused();
if (_isVisible) {
onStop();
}
}
break;
case AppLifecycleState.inactive:
onAppInactive();
break;
case AppLifecycleState.detached:
onAppDetached();
break;
case AppLifecycleState.hidden:
onAppHidden();
break;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isVisible) {
_isVisible = true;
onResume();
// Gọi onStart sau frame tiếp theo
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isVisible && !_isPaused) {
onStart();
}
});
}
}
@override
void didUpdateWidget(Screen oldWidget) {
super.didUpdateWidget(oldWidget);
// Gọi khi widget được update (có thể do navigation, state changes)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isVisible && !_isPaused) {
onStart();
}
});
}
// MARK: - Flutter Lifecycle Hooks (Override these in your screens)
/// Called when the widget is first inserted into the tree (similar to viewDidLoad in iOS)
void onInit() {
// Override in subclasses
}
/// Called when the widget is about to become visible (similar to viewWillAppear in iOS)
void onResume() {
// Override in subclasses
}
/// Called when the widget has become visible (similar to viewDidAppear in iOS)
void onStart() {
// Override in subclasses
}
/// Called when the widget is about to become invisible (similar to viewWillDisappear in iOS)
void onPause() {
// Override in subclasses
}
/// Called when the widget has become invisible (similar to viewDidDisappear in iOS)
void onStop() {
// Override in subclasses
}
/// Called when the widget is removed from the tree (similar to viewDidUnload in iOS)
void onDestroy() {
// Override in subclasses
}
/// Called when app becomes active (similar to applicationDidBecomeActive in iOS)
void onAppResumed() {
// Override in subclasses
}
/// Called when app becomes inactive (similar to applicationWillResignActive in iOS)
void onAppPaused() {
// Override in subclasses
}
/// Called when app becomes inactive (similar to applicationWillResignActive in iOS)
void onAppInactive() {
// Override in subclasses
}
/// Called when app is detached (similar to applicationWillTerminate in iOS)
void onAppDetached() {
// Override in subclasses
}
/// Called when app is hidden (similar to applicationDidEnterBackground in iOS)
void onAppHidden() {
// Override in subclasses
}
showPopup({
void showPopup({
required PopupDataModel data,
bool? barrierDismissibl,
bool showCloseButton = false,
......@@ -34,7 +166,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen> {
);
}
showAlert({
void showAlert({
required DataAlertModel data,
bool? barrierDismissibl,
bool showCloseButton = true,
......@@ -46,7 +178,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen> {
);
}
showAlertError({
void showAlertError({
required String content,
bool? barrierDismissible,
String headerImage = "assets/images/ic_pipi_03.png",
......@@ -80,7 +212,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen> {
);
}
hideKeyboard() {
void hideKeyboard() {
FocusScope.of(context).unfocus();
}
......
......@@ -37,11 +37,24 @@ class BaseViewModel extends GetxController with WidgetsBindingObserver {
}
}
showLoading({int timeout = Constants.loadingTimeoutSeconds}) {
void showProgressIndicator() {
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
}
void hideProgressIndicator() {
if (Get.isDialogOpen ?? false) {
Get.back();
}
}
void showLoading({int timeout = Constants.loadingTimeoutSeconds}) {
AppLoading().show(timeout: Duration(seconds: timeout));
}
hideLoading() {
void hideLoading() {
AppLoading().hide();
}
}
......@@ -34,6 +34,7 @@ class APIPaths {//sandbox
static const String getGames = "/campaign/api/v3.0/games";
static const String verifyOrderProduct = "/order/api/v1.0/verify-product";
static const String getGameDetail = "/campaign/api/v3.0/games/%@/play";
static const String submitGameCard = "/campaign/api/v3.0/games/%@/%@/submit";
static const String affiliateCategoryGetList = "/affiliateCategoryGetList/1.0.0";
static const String affiliateBrandGetList = "/affiliateBrandGetList/1.0.0";
static const String affiliateProductTopSale = "/affiliateProductTopSale/1.0.0";
......@@ -110,4 +111,7 @@ class APIPaths {//sandbox
static const String transactionHistoryGetList = "/transactionHistoryGetList/1.0.0";
static const String transactionGetSummaryByDate = "/transactionGetSummaryByDate/1.0.0";
static const String getOfflineBrand = "/user/api/v2.0/offline/brand";
static const String pushNotificationDeviceUpdateToken = "/pushNotificationDeviceUpdateToken/1.0.0";
static const String myProductMarkAsUsed = "/myProductMarkAsUsed/1.0.0";
static const String myProductMarkAsNotUsedYet = "/myProductMarkAsNotUsedYet/1.0.0";
}
\ No newline at end of file
import 'dart:ffi' as MediaQuery;
import 'dart:ui' as ui;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import 'dart:io' show Platform;
class DeviceDetails {
final String? hardwareType; // "Mobile" | "Tablet" | "Desktop" | "Web"
final String? hardwareModel; // iPhone15,3 | iPad... | Samsung SM-... | ...
final String? operatingSystem; // "iOS" | "Android" | "macOS" | ...
final String? osVersion; // "17.6" | "14 (SDK 34)" | ...
final String? userDeviceName; // iOS: tên máy; Android: fallback
final String? appVersion; // build-name, ví dụ "3.2.6"
final String? appBuildNumber; // build-number, ví dụ "326"
const DeviceDetails({
this.hardwareType,
this.hardwareModel,
this.operatingSystem,
this.osVersion,
this.userDeviceName,
this.appVersion,
this.appBuildNumber,
});
Map<String, String> toMap() => {
'hardware_type': hardwareType ?? 'Unknown',
'hardware_model': hardwareModel ?? 'Unknown',
'operating_system': operatingSystem ?? 'Unknown',
'os_version': osVersion ?? 'Unknown',
'user_device_name': userDeviceName ?? 'Unknown',
'app_version': appVersion ?? 'Unknown',
'app_build_number': appBuildNumber ?? 'Unknown',
};
@override
String toString() => toMap().toString();
}
class DeviceInfo {
static Future<String> getDeviceId() async {
......@@ -12,4 +51,84 @@ class DeviceInfo {
return deviceId;
}
static Future<DeviceDetails> getDetails() async {
final deviceInfo = DeviceInfoPlugin();
final pkg = await PackageInfo.fromPlatform();
// OS + version
String os, osVersion = '';
if (kIsWeb) {
os = 'Web';
final web = await deviceInfo.webBrowserInfo;
osVersion = [
web.browserName.name,
if ((web.appVersion ?? '').isNotEmpty) '(${web.appVersion})',
].where((e) => e.toString().isNotEmpty).join(' ');
} else if (Platform.isIOS) {
os = 'iOS';
final ios = await deviceInfo.iosInfo;
osVersion = ios.systemVersion;
} else if (Platform.isAndroid) {
os = 'Android';
final and = await deviceInfo.androidInfo;
final rel = and.version.release;
final sdk = and.version.sdkInt.toString();
osVersion = sdk.isEmpty ? rel : '$rel (SDK $sdk)';
} else {
os = Platform.operatingSystem; // macOS/linux/windows
osVersion = Platform.operatingSystemVersion;
}
// Model + userDeviceName + iPad fallback
String hardwareModel = 'Unknown';
String userDeviceName = '';
bool fallbackIsTablet = false;
if (kIsWeb) {
hardwareModel = 'Browser';
} else if (Platform.isIOS) {
final ios = await deviceInfo.iosInfo;
hardwareModel = ios.utsname.machine;
userDeviceName = ios.name;
fallbackIsTablet =
(ios.model.toLowerCase().contains('ipad')) || (ios.utsname.machine.toLowerCase().startsWith('ipad'));
} else if (Platform.isAndroid) {
final and = await deviceInfo.androidInfo;
final brand = and.brand.trim();
final model = and.model.trim();
hardwareModel = [brand, model].where((e) => e.isNotEmpty).join(' ');
userDeviceName = and.device;
fallbackIsTablet = false;
}
final hardwareType = _detectHardwareTypeWithoutContext(os: os, fallbackIsTablet: fallbackIsTablet);
return DeviceDetails(
hardwareType: hardwareType,
hardwareModel: hardwareModel,
operatingSystem: os,
osVersion: osVersion,
userDeviceName: userDeviceName,
appVersion: pkg.version,
appBuildNumber: pkg.buildNumber,
);
}
/// Không cần BuildContext: dùng kích thước của view đầu tiên từ PlatformDispatcher.
/// Quy ước: shortestSide >= 600 logical pixels => Tablet.
static String _detectHardwareTypeWithoutContext({required String os, required bool fallbackIsTablet}) {
if (os == 'Web') return 'Web';
if (os != 'iOS' && os != 'Android') return 'Desktop';
try {
final views = ui.PlatformDispatcher.instance.views;
final view = views.isNotEmpty ? views.first : ui.PlatformDispatcher.instance.implicitView;
if (view != null) {
final logicalSize = view.physicalSize / view.devicePixelRatio;
final shortest = logicalSize.shortestSide;
return shortest >= 600 ? 'Tablet' : 'Mobile';
}
} catch (_) {
// ignore and use fallback
}
return fallbackIsTablet ? 'Tablet' : 'Mobile';
}
}
......@@ -8,6 +8,11 @@ import '../screen/webview/web_view_screen.dart';
import '../shared/router_gage.dart';
import 'directional_action_type.dart';
class Defines {
static const String actionType = 'click_action_type';
static const String actionParams = 'click_action_param';
}
class DirectionalScreen {
final String? clickActionType;
final String? clickActionParam;
......
import 'dart:convert';
/// Trả về Map<String, dynamic> từ payload của notification response.
/// - Hỗ trợ JSON thuần
/// - Hỗ trợ JSON bị URL-encode
/// - Fallback: parse kiểu querystring: a=1&b=2
Map<String, dynamic> parseNotificationPayload(String? payload) {
if (payload == null) return {};
String s = payload.trim();
if (s.isEmpty) return {};
// 1) Nếu là JSON thuần
Map<String, dynamic>? tryJson(String text) {
try {
final v = jsonDecode(text);
if (v is Map) {
// ép key về String
return v.map((k, val) => MapEntry(k.toString(), val));
}
} catch (_) {}
return null;
}
// a) thử trực tiếp
var m = tryJson(s);
if (m != null) return m;
// b) thử URL-decode rồi parse JSON lại
final decoded = Uri.decodeFull(s);
m = tryJson(decoded);
if (m != null) return m;
// 2) Fallback querystring: key1=val1&key2=val2
try {
// Bỏ ngoặc {..} nếu server lỡ bọc
final q = s.startsWith('{') && s.endsWith('}') ? s.substring(1, s.length - 1) : s;
final map = Uri.splitQueryString(q, encoding: utf8);
return map.map((k, v) => MapEntry(k, v));
} catch (_) {
// 3) Fallback kiểu Dart Map.toString(): {a: 1, b: 2}
try {
final trimmed = s.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
final inner = trimmed.substring(1, trimmed.length - 1).trim();
if (inner.isEmpty) return <String, dynamic>{};
final Map<String, dynamic> out = {};
// Tách theo dấu phẩy cấp 1 (không xử lý nested phức tạp)
for (final pair in inner.split(',')) {
final idx = pair.indexOf(':');
if (idx <= 0) continue;
final key = pair.substring(0, idx).trim();
final val = pair.substring(idx + 1).trim();
// Bỏ quote nếu có
String normalize(String v) {
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith('\'') && v.endsWith('\''))) {
return v.substring(1, v.length - 1);
}
return v;
}
final nk = normalize(key);
final nv = normalize(val);
out[nk] = nv;
}
if (out.isNotEmpty) return out;
}
} catch (_) {}
// Cùng lắm trả string gốc
return {'payload': s};
}
}
import 'package:flutter/foundation.dart';
import '../directional/directional_action_type.dart';
import '../directional/directional_screen.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
enum NotificationType { fcm, moengage }
class NotificationRouter {
static Future<void> handleRemoteMessage(RemoteMessage m) async {
final data = PushNotification(info: m.data);
handleDirectionNotification(data);
}
static Future<void> handleDirectionNotification(PushNotification notification) async {
if (kDebugMode) {
print('Parsed action type: ${notification.screenDirectional?.clickActionType}');
print('Parsed action param: ${notification.screenDirectional?.clickActionParam}');
}
notification.screenDirectional?.begin();
}
}
class PushNotification {
final Map<String, dynamic> info;
PushNotification({required Map info})
: info = info.map((k, v) => MapEntry(k.toString(), v));
NotificationType get type =>
info.containsKey('moengage') ? NotificationType.moengage : NotificationType.fcm;
String get title {
final rootTitle = _asString(info['title']) ?? _asString(info['gcm.notification.title']);
if (rootTitle != null) return rootTitle;
final aps = _asMap(info['aps']);
final alert = _asMap(aps?['alert']);
return _asString(alert?['title']) ?? '';
}
String get body {
final rootBody = _asString(info['body']) ?? _asString(info['gcm.notification.body']);
if (rootBody != null) return rootBody;
final aps = _asMap(info['aps']);
final alert = _asMap(aps?['alert']);
return _asString(alert?['body']) ?? '';
}
String? get timeString => _asString(info['time']);
String? get id => _asString(info['notification_id']);
DirectionalScreen? get screenDirectional {
if (kDebugMode) {
print('=== PARSING NOTIFICATION DATA ===');
print('Raw info: $info');
}
String? name;
String? param;
final extra = _asMap(info['app_extra']);
if (kDebugMode) print('App extra: $extra');
final screenData = _asMap(extra?['screenData']);
if (kDebugMode) print('Screen data: $screenData');
if (screenData != null) {
name = _asString(screenData[Defines.actionType]);
param = _asString(screenData[Defines.actionParams]);
if (kDebugMode) print('From screenData - name: $name, param: $param');
} else {
name = _asString(info[Defines.actionType]);
param = _asString(info[Defines.actionParams]);
if (kDebugMode) print('From info - name: $name, param: $param');
}
if (name != null || param != null) {
if (kDebugMode) print('Building DirectionalScreen with name: $name, param: $param');
return DirectionalScreen.build(clickActionType: name, clickActionParam: param);
}
if (kDebugMode) {
print('No action data found, using default notification screen with ID: $id');
print('Title: $title, Body: $body');
}
return DirectionalScreen.buildByName(name: DirectionalScreenName.notification, clickActionParam: id);
// TODO handel title + body
}
// ===== Helpers =====
static String? _asString(dynamic v) => v is String ? v : null;
static Map<String, dynamic>? _asMap(dynamic v) {
if (v is Map) {
// ép key về String để thao tác dễ hơn
return v.map((k, val) => MapEntry(k.toString(), val));
}
return null;
}
}
\ No newline at end of file
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