feat: 对齐常驻商城与七鱼客服交互
- 同步商城 H5 登录态与原生 token - 完善商城/原生 tab 切换、动画及返回交互 - 修复七鱼客服键盘避让和状态栏恢复 - 调整开屏跳过按钮与登录入口交互
This commit is contained in:
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 [],
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../common/network/dio_provider.dart';
|
||||
import '../../ads/data/ads_api_client.dart';
|
||||
|
||||
class AppMallAuthToken {
|
||||
const AppMallAuthToken({
|
||||
required this.authToken,
|
||||
required this.expiresIn,
|
||||
required this.sourceApp,
|
||||
required this.scene,
|
||||
});
|
||||
|
||||
final String authToken;
|
||||
final int expiresIn;
|
||||
final String sourceApp;
|
||||
final String scene;
|
||||
}
|
||||
|
||||
class MallAuthApi {
|
||||
const MallAuthApi(this._dio);
|
||||
final Dio _dio;
|
||||
|
||||
Future<AppMallAuthToken> issueAppMallAuthToken() async {
|
||||
final response = await _dio.post<List<int>>(
|
||||
'/api/v1/auth/mall/app-token',
|
||||
data: const <int>[],
|
||||
options: protobufOptions(),
|
||||
);
|
||||
final reply = ProtoMessage(response.data ?? const <int>[]);
|
||||
final token = reply.string(1).trim();
|
||||
if (token.isEmpty) throw const FormatException('Missing mall auth token');
|
||||
return AppMallAuthToken(
|
||||
authToken: token,
|
||||
expiresIn: reply.integer(2),
|
||||
sourceApp: reply.string(3),
|
||||
scene: reply.string(4),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final mallAuthApiProvider = Provider(
|
||||
(ref) => MallAuthApi(ref.watch(dioProvider)),
|
||||
);
|
||||
Reference in new issue
Block a user