修复:禁止截屏功能
This commit is contained in:
parent
3edf55cacd
commit
65439a66b2
@ -71,6 +71,7 @@
|
|||||||
"firstPurchase": "qlw4fp",
|
"firstPurchase": "qlw4fp",
|
||||||
"purchase": "b2ms4n",
|
"purchase": "b2ms4n",
|
||||||
"register": "2k7vm5",
|
"register": "2k7vm5",
|
||||||
|
"paymentFailed": "",
|
||||||
"price_599": "mtzzqk",
|
"price_599": "mtzzqk",
|
||||||
"price_999": "m9ivl1",
|
"price_999": "m9ivl1",
|
||||||
"price_1999": "kp7a52",
|
"price_1999": "kp7a52",
|
||||||
@ -80,7 +81,7 @@
|
|||||||
"extConfig": {
|
"extConfig": {
|
||||||
"keys": {
|
"keys": {
|
||||||
"showVideoMenu": ["go_run", "need_wait"],
|
"showVideoMenu": ["go_run", "need_wait"],
|
||||||
"allowScreenshot": ["screen"],
|
"forbidScreenshot": ["screen"],
|
||||||
"blockScreenshot": ["safe_area"],
|
"blockScreenshot": ["safe_area"],
|
||||||
"allowThirdPartyPayment": ["san_fang", "lucky"],
|
"allowThirdPartyPayment": ["san_fang", "lucky"],
|
||||||
"privacyUrl": ["privacy"],
|
"privacyUrl": ["privacy"],
|
||||||
|
|||||||
@ -171,7 +171,7 @@ lib/
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid |
|
| `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid |
|
||||||
| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info(**建议仅在为 `true` 时展示核心业务 UI**) |
|
| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info(**建议仅在为 `true` 时展示核心业务 UI**) |
|
||||||
| `ExtConfigData` | `showVideoMenu`、`allowScreenshot`、`allowThirdPartyPayment`、`privacyUrl`、`agreementUrl`、`items` |
|
| `ExtConfigData` | `showVideoMenu`、`forbidScreenshot`、`allowThirdPartyPayment`、`privacyUrl`、`agreementUrl`、`items` |
|
||||||
| `ExtConfigItem` | 单项:`image`、`image_fix`、`img_need`、`cost`、`title`、`params` / `detail`;`taskExt` ⇒ `params ?? detail` |
|
| `ExtConfigItem` | 单项:`image`、`image_fix`、`img_need`、`cost`、`title`、`params` / `detail`;`taskExt` ⇒ `params ?? detail` |
|
||||||
| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id |
|
| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id |
|
||||||
| `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab |
|
| `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab |
|
||||||
@ -182,7 +182,7 @@ lib/
|
|||||||
| 语义 | 默认候选键(首个存在则生效) |
|
| 语义 | 默认候选键(首个存在则生效) |
|
||||||
|------|------------------------------|
|
|------|------------------------------|
|
||||||
| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run`、`need_wait` |
|
| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run`、`need_wait` |
|
||||||
| 允许截屏 | `screen`;若无则看 `safe_area`(`true` ⇒ 不允许截屏) |
|
| 是否禁止截屏(逻辑 [ExtConfigData.forbidScreenshot]) | `screen`;若无则看 `safe_area`(均为 **`true` = 禁止截屏**) |
|
||||||
| 允许第三方支付 | `san_fang`、`lucky` |
|
| 允许第三方支付 | `san_fang`、`lucky` |
|
||||||
| 隐私 / 协议 URL | `privacy`、`agreement` |
|
| 隐私 / 协议 URL | `privacy`、`agreement` |
|
||||||
| items 数组 | `items` |
|
| items 数组 | `items` |
|
||||||
@ -211,7 +211,7 @@ lib/
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
首页逻辑建议(对齐 `app_client`):`await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab,分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `shouldPreventCapture` 或自行根据 `allowScreenshot` 调用宿主侧防护(框架不强制依赖 `screen_secure`)。
|
首页逻辑建议(对齐 `app_client`):`await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab,分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `forbidScreenshot` 驱动宿主侧防护(如 `screen_secure`;框架不强制依赖)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
| 子节 | 说明 |
|
| 子节 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `keys.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 |
|
| `keys.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 |
|
||||||
| `keys.allowScreenshot` | 直接表示「允许截屏」的键,如 `["screen"]` |
|
| `keys.forbidScreenshot` | 线网为 `true` 时表示禁止截屏的键,如 `["screen"]`(兼容旧键名 `allowScreenshot`) |
|
||||||
| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` |
|
| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` |
|
||||||
| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` |
|
| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` |
|
||||||
| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 |
|
| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 |
|
||||||
|
|||||||
41
lib/app.dart
41
lib/app.dart
@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -8,15 +10,50 @@ import 'core/theme/app_colors.dart';
|
|||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'features/shell/main_screen.dart';
|
import 'features/shell/main_screen.dart';
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatefulWidget {
|
||||||
const App({super.key, required this.title});
|
const App({super.key, required this.title});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<App> createState() => _AppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_reportFacebookActivateApp('first_frame');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 与 app_client 一致:关闭 FB 自动 App Events 时手动 [FacebookService.activateApp]。
|
||||||
|
void _reportFacebookActivateApp(String reason) {
|
||||||
|
FacebookService.activateApp();
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[App] Facebook activateApp ($reason)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_reportFacebookActivateApp('lifecycle_resumed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: title,
|
title: widget.title,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: buildFunyMeeTheme(),
|
theme: buildFunyMeeTheme(),
|
||||||
home: const MainScreen(),
|
home: const MainScreen(),
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ import 'package:client_proxy_framework/client_proxy_framework.dart';
|
|||||||
import 'package:crypto/crypto.dart' show md5;
|
import 'package:crypto/crypto.dart' show md5;
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:screen_secure/screen_secure.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../user/user_state.dart';
|
import '../user/user_state.dart';
|
||||||
@ -58,6 +60,11 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
|
|||||||
avatar: data.avatar,
|
avatar: data.avatar,
|
||||||
userName: data.userName,
|
userName: data.userName,
|
||||||
);
|
);
|
||||||
|
unawaited(
|
||||||
|
AnalyticsEvents.trackRegisterIfNeeded(
|
||||||
|
firstRegister: data.firstRegister == true,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -77,11 +84,77 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
|
|||||||
class AuthService {
|
class AuthService {
|
||||||
static final _authCallbacks = AppAuthCallbacks();
|
static final _authCallbacks = AppAuthCallbacks();
|
||||||
|
|
||||||
|
static var _screenSecureListenerBound = false;
|
||||||
|
|
||||||
|
/// [ExtConfigRuntime.data] 初次为 `null` 时触发的 `_applyScreenSecure(null)` 与后续
|
||||||
|
/// common_info 触发的 `_applyScreenSecure(…)` 均为异步:若前者后完成会错误地关闭防护,故用序号丢弃过期任务。
|
||||||
|
static int _screenSecureApplyGeneration = 0;
|
||||||
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
FrameworkAuthService.init(_authCallbacks);
|
FrameworkAuthService.init(_authCallbacks);
|
||||||
|
_bindScreenSecureToExtConfig();
|
||||||
await FrameworkAuthService.start();
|
await FrameworkAuthService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 与 [app_client] `AuthService.runWithNativeMediaPicker` 一致:系统相册/相机返回后若开启防截屏,
|
||||||
|
/// 部分机型会黑屏;选图前后临时关闭防护,结束后再按 [ExtConfigRuntime] 恢复。
|
||||||
|
static Future<T> runWithNativeMediaPicker<T>(Future<T> Function() action) async {
|
||||||
|
if (defaultTargetPlatform != TargetPlatform.android &&
|
||||||
|
defaultTargetPlatform != TargetPlatform.iOS) {
|
||||||
|
return await action();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ScreenSecure.disableScreenshotBlock();
|
||||||
|
await ScreenSecure.disableScreenRecordBlock();
|
||||||
|
} on ScreenSecureException catch (e) {
|
||||||
|
debugPrint('[AuthService] native media picker: disable failed: ${e.message}');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await action();
|
||||||
|
} finally {
|
||||||
|
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _bindScreenSecureToExtConfig() {
|
||||||
|
if (_screenSecureListenerBound) return;
|
||||||
|
_screenSecureListenerBound = true;
|
||||||
|
void listener() {
|
||||||
|
unawaited(_applyScreenSecure(ExtConfigRuntime.data.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtConfigRuntime.data.addListener(listener);
|
||||||
|
// 勿在此处同步调用 listener():`data == null` 的 disable 若晚于 common_info 的 enable 完成,会把防截屏关掉。
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [ExtConfigData]:`screen` / `safe_area` 等键在 [ExtConfigKeySchema] 下解析为 [ExtConfigData.forbidScreenshot];
|
||||||
|
/// 为 `true` 时启用系统截屏/录屏防护(与 app_client `safe_area` 语义对齐)。
|
||||||
|
static Future<void> _applyScreenSecure(ExtConfigData? ext) async {
|
||||||
|
if (defaultTargetPlatform != TargetPlatform.android &&
|
||||||
|
defaultTargetPlatform != TargetPlatform.iOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final gen = ++_screenSecureApplyGeneration;
|
||||||
|
final block = ext?.forbidScreenshot == true;
|
||||||
|
try {
|
||||||
|
await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false);
|
||||||
|
if (gen != _screenSecureApplyGeneration) return;
|
||||||
|
if (block) {
|
||||||
|
await ScreenSecure.enableScreenshotBlock();
|
||||||
|
await ScreenSecure.enableScreenRecordBlock();
|
||||||
|
} else {
|
||||||
|
await ScreenSecure.disableScreenshotBlock();
|
||||||
|
await ScreenSecure.disableScreenRecordBlock();
|
||||||
|
}
|
||||||
|
if (gen != _screenSecureApplyGeneration) return;
|
||||||
|
if (kDebugMode && block) {
|
||||||
|
debugPrint('[AuthService] ScreenSecure: enabled (forbidScreenshot)');
|
||||||
|
}
|
||||||
|
} on ScreenSecureException catch (e) {
|
||||||
|
debugPrint('[AuthService] ScreenSecure: ${e.message}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
|
static Future<void> get loginComplete => FrameworkAuthService.loginComplete;
|
||||||
|
|
||||||
/// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。
|
/// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../../core/app_env.dart';
|
import '../../core/app_env.dart';
|
||||||
|
import '../../core/auth/auth_service.dart';
|
||||||
import '../../core/open_purchase_store.dart';
|
import '../../core/open_purchase_store.dart';
|
||||||
import '../../core/user/user_state.dart';
|
import '../../core/user/user_state.dart';
|
||||||
import '../../design/pencil_theme.dart';
|
import '../../design/pencil_theme.dart';
|
||||||
@ -142,7 +143,9 @@ class _GenerateScreenState extends State<GenerateScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final source = await _showPickImageSourceSheet(context);
|
final source = await _showPickImageSourceSheet(context);
|
||||||
if (source == null || !mounted) return;
|
if (source == null || !mounted) return;
|
||||||
final x = await _picker.pickImage(source: source, imageQuality: 92);
|
final x = await AuthService.runWithNativeMediaPicker(
|
||||||
|
() => _picker.pickImage(source: source, imageQuality: 92),
|
||||||
|
);
|
||||||
if (x == null || !mounted) return;
|
if (x == null || !mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (slot == 0) {
|
if (slot == 0) {
|
||||||
@ -619,7 +622,7 @@ class _GenerateScreenState extends State<GenerateScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'Est. cost · $_estimatedCost credits',
|
'cost · $_estimatedCost credits',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@ -627,22 +630,6 @@ class _GenerateScreenState extends State<GenerateScreen> {
|
|||||||
color: PencilTheme.stone600,
|
color: PencilTheme.stone600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
InkWell(
|
|
||||||
onTap: () => openPurchaseStore(context),
|
|
||||||
child: Text(
|
|
||||||
'Balance · ${credits.toStringAsFixed(2)}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: GoogleFonts.inter(
|
|
||||||
fontSize: 12,
|
|
||||||
color: PencilTheme.inkSoft,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
decorationColor: PencilTheme.inkSoft.withValues(
|
|
||||||
alpha: 0.45,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -73,7 +73,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: 28 +
|
bottom:
|
||||||
|
28 +
|
||||||
(widget.isRootTab
|
(widget.isRootTab
|
||||||
? PencilTheme.mainTabBottomChromeReserve(context)
|
? PencilTheme.mainTabBottomChromeReserve(context)
|
||||||
: 0),
|
: 0),
|
||||||
@ -154,59 +155,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||||
child: _menuCard(context),
|
child: _menuCard(context),
|
||||||
),
|
),
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) => const PurchaseScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Buy credits',
|
|
||||||
style: GoogleFonts.inter(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: PencilTheme.underlineGold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await UserAccountRefresh.fetchAndNotify(
|
|
||||||
app: currentBackendAppType(),
|
|
||||||
userId: UserState.userId.value,
|
|
||||||
onAccount: (a) {
|
|
||||||
if (a.credits != null) {
|
|
||||||
UserState.setCredits(a.credits!);
|
|
||||||
}
|
|
||||||
if (a.avatar != null) {
|
|
||||||
UserState.setAvatar(a.avatar!);
|
|
||||||
}
|
|
||||||
if (a.userName != null) {
|
|
||||||
UserState.setUserName(a.userName!);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFailure: (m) {
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(SnackBar(content: Text(m)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Refresh',
|
|
||||||
style: GoogleFonts.inter(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: PencilTheme.stone600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -219,8 +168,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _avatarFallback() {
|
Widget _avatarFallback() {
|
||||||
return Icon(Icons.person_rounded,
|
return Icon(
|
||||||
size: 44, color: PencilTheme.profileAvatarIcon);
|
Icons.person_rounded,
|
||||||
|
size: 44,
|
||||||
|
color: PencilTheme.profileAvatarIcon,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatCredits(int c) {
|
String _formatCredits(int c) {
|
||||||
@ -235,17 +187,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
required String? url,
|
required String? url,
|
||||||
}) {
|
}) {
|
||||||
if (url == null || url.trim().isEmpty) {
|
if (url == null || url.trim().isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(content: Text('Link not configured')),
|
context,
|
||||||
);
|
).showSnackBar(const SnackBar(content: Text('Link not configured')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Navigator.of(context).push<void>(
|
Navigator.of(context).push<void>(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder: (_) => AppWebViewScreen(
|
builder: (_) => AppWebViewScreen(title: title, initialUrl: url.trim()),
|
||||||
title: title,
|
|
||||||
initialUrl: url.trim(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -288,17 +237,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
_divider(),
|
_divider(),
|
||||||
_row('Version', value: 'v$_version'),
|
_row('Version', value: 'v$_version'),
|
||||||
_divider(),
|
_divider(),
|
||||||
_row('Delete account',
|
_row(
|
||||||
|
'Delete account',
|
||||||
danger: true,
|
danger: true,
|
||||||
trailing: Icons.chevron_right_rounded,
|
trailing: Icons.chevron_right_rounded,
|
||||||
onTap: () => _delete(context)),
|
onTap: () => _delete(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _row(String title,
|
Widget _row(
|
||||||
{IconData? trailing, String? value, bool danger = false, VoidCallback? onTap}) {
|
String title, {
|
||||||
|
IconData? trailing,
|
||||||
|
String? value,
|
||||||
|
bool danger = false,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
}) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -310,17 +266,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: value != null
|
trailing: value != null
|
||||||
? Text(value,
|
? Text(
|
||||||
|
value,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: const Color(0xFF78716C)))
|
color: const Color(0xFF78716C),
|
||||||
: Icon(trailing,
|
),
|
||||||
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E)),
|
)
|
||||||
|
: Icon(
|
||||||
|
trailing,
|
||||||
|
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _divider() =>
|
Widget _divider() => Container(height: 1, color: const Color(0xFFF5F5F4));
|
||||||
Container(height: 1, color: const Color(0xFFF5F5F4));
|
|
||||||
|
|
||||||
Future<void> _delete(BuildContext context) async {
|
Future<void> _delete(BuildContext context) async {
|
||||||
final ok = await showDeleteAccountConfirmationFlow(context);
|
final ok = await showDeleteAccountConfirmationFlow(context);
|
||||||
@ -335,12 +295,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
UserState.clear();
|
UserState.clear();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Account deleted. Please restart the app and sign in again.')),
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Account deleted. Please restart the app and sign in again.',
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(res.msg)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(res.msg)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,6 +212,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_paying = false;
|
_paying = false;
|
||||||
_selectedIndex = null;
|
_selectedIndex = null;
|
||||||
});
|
});
|
||||||
|
AnalyticsEvents.trackPaymentFailed();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
@ -230,6 +231,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_paying = false;
|
_paying = false;
|
||||||
_selectedIndex = null;
|
_selectedIndex = null;
|
||||||
});
|
});
|
||||||
|
AnalyticsEvents.trackPaymentFailed();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('No payment methods available.')),
|
const SnackBar(content: Text('No payment methods available.')),
|
||||||
);
|
);
|
||||||
@ -256,7 +258,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
});
|
});
|
||||||
|
|
||||||
final sink = _PurchaseSink(
|
final innerSink = _PurchaseSink(
|
||||||
context: context,
|
context: context,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -272,6 +274,10 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
final sink = PaymentSettlementSinkWithAnalytics(
|
||||||
|
inner: innerSink,
|
||||||
|
analyticsProduct: item,
|
||||||
|
);
|
||||||
|
|
||||||
final outcome = await ThirdPartyCheckoutCoordinator.createOrder(
|
final outcome = await ThirdPartyCheckoutCoordinator.createOrder(
|
||||||
userId: uid,
|
userId: uid,
|
||||||
@ -288,6 +294,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_paying = false;
|
_paying = false;
|
||||||
_selectedIndex = null;
|
_selectedIndex = null;
|
||||||
});
|
});
|
||||||
|
AnalyticsEvents.trackPaymentFailed();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(outcome.message ?? 'Create order failed')),
|
SnackBar(content: Text(outcome.message ?? 'Create order failed')),
|
||||||
);
|
);
|
||||||
@ -319,6 +326,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_paying = false;
|
_paying = false;
|
||||||
_selectedIndex = null;
|
_selectedIndex = null;
|
||||||
});
|
});
|
||||||
|
AnalyticsEvents.trackPaymentFailed();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Invalid payment response.')),
|
const SnackBar(content: Text('Invalid payment response.')),
|
||||||
);
|
);
|
||||||
@ -367,6 +375,8 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnalyticsEvents.trackTierSelection(item);
|
||||||
|
|
||||||
final ext = ExtConfigRuntime.data.value;
|
final ext = ExtConfigRuntime.data.value;
|
||||||
if (_useThirdPartyFromExt(ext)) {
|
if (_useThirdPartyFromExt(ext)) {
|
||||||
await _onBuyThirdParty(item, index);
|
await _onBuyThirdParty(item, index);
|
||||||
@ -389,7 +399,7 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
});
|
});
|
||||||
|
|
||||||
final sink = _PurchaseSink(
|
final innerSink = _PurchaseSink(
|
||||||
context: context,
|
context: context,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -403,6 +413,10 @@ class _PurchaseScreenState extends State<PurchaseScreen> {
|
|||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
final sink = PaymentSettlementSinkWithAnalytics(
|
||||||
|
inner: innerSink,
|
||||||
|
analyticsProduct: item,
|
||||||
|
);
|
||||||
|
|
||||||
await NativeIapCoordinator.purchaseGooglePlay(
|
await NativeIapCoordinator.purchaseGooglePlay(
|
||||||
sink: sink,
|
sink: sink,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../core/auth/auth_service.dart';
|
||||||
import '../../design/pencil_theme.dart';
|
import '../../design/pencil_theme.dart';
|
||||||
import '../../widgets/pencil_chrome.dart';
|
import '../../widgets/pencil_chrome.dart';
|
||||||
import 'report_feedback_upload.dart';
|
import 'report_feedback_upload.dart';
|
||||||
@ -39,9 +40,11 @@ class _ReportScreenState extends State<ReportScreen> {
|
|||||||
|
|
||||||
Future<void> _pickImage() async {
|
Future<void> _pickImage() async {
|
||||||
if (_submitting) return;
|
if (_submitting) return;
|
||||||
final x = await _picker.pickImage(
|
final x = await AuthService.runWithNativeMediaPicker(
|
||||||
|
() => _picker.pickImage(
|
||||||
source: ImageSource.gallery,
|
source: ImageSource.gallery,
|
||||||
imageQuality: 85,
|
imageQuality: 85,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (x == null || !mounted) return;
|
if (x == null || !mounted) return;
|
||||||
setState(() => _imageFile = File(x.path));
|
setState(() => _imageFile = File(x.path));
|
||||||
|
|||||||
@ -743,6 +743,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
screen_secure:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: screen_secure
|
||||||
|
sha256: "9660d0a285f7e27d482333779153e3cb17c5fd929e15373e51abad63810df7b4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -29,6 +29,7 @@ dependencies:
|
|||||||
package_info_plus: ^8.1.2
|
package_info_plus: ^8.1.2
|
||||||
webview_flutter: ^4.13.1
|
webview_flutter: ^4.13.1
|
||||||
gal: ^2.3.2
|
gal: ^2.3.2
|
||||||
|
screen_secure: ^1.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user