feat: 对齐常驻商城与七鱼客服交互

- 同步商城 H5 登录态与原生 token
- 完善商城/原生 tab 切换、动画及返回交互
- 修复七鱼客服键盘避让和状态栏恢复
- 调整开屏跳过按钮与登录入口交互
This commit is contained in:
jingyun committed 2026-09-07 15:47:01 +08:00
1 parent b4343ea528
commit 3b089e2205
37 files changed
+2970 -375

No files matched your search

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../web/application/mall_web_host_controller.dart';
import 'mall_tab_config.dart';
enum MallEntryOpenResult { opened, unavailable, failed, alreadyOpening }
class MallEntryController {
MallEntryController(this._ref);
final Ref _ref;
bool _opening = false;
Future<MallEntryOpenResult> open(
BuildContext context, {
MallTabConfig? initialConfig,
}) async {
if (_opening) return MallEntryOpenResult.alreadyOpening;
_opening = true;
try {
var config = initialConfig;
if (config?.isVisible != true) {
config = await _ref.read(mallTabConfigProvider.notifier).ensureLoaded();
}
if (config?.isVisible != true || !context.mounted) {
return MallEntryOpenResult.unavailable;
}
final clickId = await _ref
.read(mallTabConfigProvider.notifier)
.recordClick();
if (clickId == null || !context.mounted) {
return MallEntryOpenResult.unavailable;
}
_ref
.read(mallClickAttributionProvider.notifier)
.state = MallClickAttribution(
adId: config!.adId,
trackingId: config.trackingId,
clickId: clickId,
);
final uri = config.attributedUri(
clickId: clickId,
isDark: Theme.of(context).brightness == Brightness.dark,
);
if (uri == null) return MallEntryOpenResult.unavailable;
// The bottom mall entry is always resident. Server open-mode metadata is
// still retained for attribution, while explicit links inside H5 decide
// their own external handoff through the WebView navigation policy.
MallWebHostController.instance.open(uri);
return MallEntryOpenResult.opened;
} catch (_) {
return MallEntryOpenResult.failed;
} finally {
_opening = false;
}
}
}
final mallEntryControllerProvider = Provider((ref) => MallEntryController(ref));
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
@@ -145,6 +146,62 @@ final mallTabConfigProvider =
(ref) => MallTabConfigController(ref.watch(dioProvider)),
);
class MallClickAttribution {
const MallClickAttribution({
required this.adId,
required this.trackingId,
required this.clickId,
});
final String adId;
final String trackingId;
final String clickId;
}
final mallClickAttributionProvider = StateProvider<MallClickAttribution?>(
(ref) => null,
);
Map<String, dynamic> buildEnterWebMallBridgeArguments({
required Uri currentUri,
required bool logged,
required bool isDark,
String? authToken,
String? scene,
String? sourceApp,
MallClickAttribution? attribution,
}) {
final query = currentUri.queryParameters;
final resolvedSourceApp = sourceApp?.trim().isNotEmpty == true
? sourceApp!.trim()
: mallSourceApp;
final arguments = <String, dynamic>{
...query,
'from': resolvedSourceApp,
'source_app': resolvedSourceApp,
'source_entry': query['source_entry'] ?? 'mall_tab',
'placement': query['placement'] ?? 'mall_tab',
'hook_type': query['hook_type'] ?? 'mall_tab',
'target_app': query['target_app'] ?? 'mall',
'logged': logged,
'isNight': isDark ? '1' : '0',
};
final adId = attribution?.adId.trim() ?? '';
if (adId.isNotEmpty) arguments['ad_id'] = adId;
final trackingId = attribution?.trackingId.trim() ?? '';
if (trackingId.isNotEmpty) arguments['tracking_id'] = trackingId;
final clickId = attribution?.clickId.trim() ?? '';
if (clickId.isNotEmpty) arguments['click_id'] = clickId;
final token = authToken?.trim() ?? '';
if (token.isNotEmpty) {
arguments['auth_token'] = token;
arguments['authToken'] = token;
}
final resolvedScene = scene?.trim() ?? '';
if (resolvedScene.isNotEmpty) arguments['scene'] = resolvedScene;
return arguments;
}
String _nonEmpty(String value, String fallback) =>
value.trim().isEmpty ? fallback : value.trim();
@@ -166,7 +223,7 @@ class _ProtoMessage {
int integer(int wanted) =>
_fields()[wanted] is int ? _fields()[wanted]! as int : 0;
String string(int wanted) => _fields()[wanted] is List<int>
? String.fromCharCodes(_fields()[wanted]! as List<int>)
? utf8.decode(_fields()[wanted]! as List<int>, allowMalformed: true)
: '';
_ProtoMessage message(int wanted) => _ProtoMessage(
_fields()[wanted] is List<int> ? _fields()[wanted]! as List<int> : const [],