fix:对齐金值,主要注销,一键登录等方面修改
This commit is contained in:
1 parent
e29ed0a2c8
commit
6997a65c96
37 files changed
+3442
-514
No files matched your search
@@ -27,23 +27,14 @@ class MallEntryController {
|
||||
if (config?.isVisible != true || !context.mounted) {
|
||||
return MallEntryOpenResult.unavailable;
|
||||
}
|
||||
final clickId = await _ref
|
||||
config = await _ref
|
||||
.read(mallTabConfigProvider.notifier)
|
||||
.recordClick();
|
||||
if (clickId == null || !context.mounted) {
|
||||
.recordClick(request: request);
|
||||
if (!config.isVisible || !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,
|
||||
entry: request,
|
||||
);
|
||||
if (uri == null) return MallEntryOpenResult.unavailable;
|
||||
// The bottom mall entry is always resident. Server open-mode metadata is
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/network/dio_provider.dart';
|
||||
import '../../../models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||
|
||||
/// 金值商城广告位的公共契约;商城是否展示完全由服务端配置决定。
|
||||
/// 标识打开商城的宿主项目。
|
||||
const mallSourceApp = 'jinzhi';
|
||||
|
||||
/// The native entry that opened the resident mall. H5 uses this context to
|
||||
/// choose the appropriate landing flow while sharing the same WebView host.
|
||||
class MallEntryRequest {
|
||||
const MallEntryRequest({
|
||||
required this.sourceEntry,
|
||||
@@ -45,11 +42,15 @@ class MallTabConfig {
|
||||
this.title = '商城',
|
||||
this.adId = '',
|
||||
this.trackingId = '',
|
||||
this.clickId = '',
|
||||
this.openMode = 'in_app',
|
||||
this.entryPromoEnabled = false,
|
||||
this.entryBadgeText = '',
|
||||
this.entryTipTitle = '',
|
||||
this.entryTipBody = '',
|
||||
this.quoteCtaEnabled = false,
|
||||
this.entryRequest = MallEntryRequest.home,
|
||||
this.allowedHosts = const <String>{},
|
||||
});
|
||||
|
||||
const MallTabConfig.disabled() : this(enabled: false, url: '');
|
||||
@@ -59,11 +60,15 @@ class MallTabConfig {
|
||||
final String title;
|
||||
final String adId;
|
||||
final String trackingId;
|
||||
final String clickId;
|
||||
final String openMode;
|
||||
final bool entryPromoEnabled;
|
||||
final String entryBadgeText;
|
||||
final String entryTipTitle;
|
||||
final String entryTipBody;
|
||||
final bool quoteCtaEnabled;
|
||||
final MallEntryRequest entryRequest;
|
||||
final Set<String> allowedHosts;
|
||||
|
||||
bool get isVisible => enabled && initialUri != null;
|
||||
bool get opensInApp => openMode == 'in_app';
|
||||
@@ -74,52 +79,80 @@ class MallTabConfig {
|
||||
entryPromoEnabled &&
|
||||
(entryTipTitle.trim().isNotEmpty || entryTipBody.trim().isNotEmpty);
|
||||
|
||||
Uri? get initialUri => attributedUri(clickId: '');
|
||||
String get from {
|
||||
final value = Uri.tryParse(url.trim())?.queryParameters['from']?.trim();
|
||||
return value == null || value.isEmpty ? mallSourceApp : value;
|
||||
}
|
||||
|
||||
Uri? get initialUri => attributedUri();
|
||||
|
||||
Uri? attributedUri({
|
||||
required String clickId,
|
||||
String? clickId,
|
||||
bool isDark = false,
|
||||
MallEntryRequest entry = MallEntryRequest.home,
|
||||
MallEntryRequest? entry,
|
||||
}) {
|
||||
final uri = Uri.tryParse(url.trim());
|
||||
if (!_isAllowed(uri)) return null;
|
||||
final effectiveClickId = clickId ?? this.clickId;
|
||||
final effectiveEntry = entry ?? entryRequest;
|
||||
return uri!.replace(
|
||||
queryParameters: {
|
||||
...uri.queryParameters,
|
||||
'from': uri.queryParameters['from']?.trim().isNotEmpty == true
|
||||
? uri.queryParameters['from']!
|
||||
: mallSourceApp,
|
||||
'source_app': mallSourceApp,
|
||||
'source_entry': entry.sourceEntry,
|
||||
'from': from,
|
||||
'source_app': from,
|
||||
'source_entry': effectiveEntry.sourceEntry,
|
||||
'placement': 'mall_tab',
|
||||
'hook_type': entry.hookType,
|
||||
if (entry.entryScene?.isNotEmpty == true)
|
||||
'entry_scene': entry.entryScene!,
|
||||
if (entry.entryMetal?.isNotEmpty == true)
|
||||
'entry_metal': entry.entryMetal!,
|
||||
'hook_type': effectiveEntry.hookType,
|
||||
'entry_scene': ?effectiveEntry.entryScene,
|
||||
'entry_metal': ?effectiveEntry.entryMetal,
|
||||
'target_app': 'mall',
|
||||
if (adId.isNotEmpty) 'ad_id': adId,
|
||||
if (trackingId.isNotEmpty) 'tracking_id': trackingId,
|
||||
if (clickId.isNotEmpty) 'click_id': clickId,
|
||||
if (clickId.isNotEmpty)
|
||||
if (effectiveClickId.isNotEmpty) 'click_id': effectiveClickId,
|
||||
if (effectiveClickId.isNotEmpty)
|
||||
'ts': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'isNight': isDark ? '1' : '0',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
MallTabConfig withClick(
|
||||
String value, {
|
||||
MallEntryRequest request = MallEntryRequest.home,
|
||||
}) => MallTabConfig(
|
||||
enabled: enabled,
|
||||
url: url,
|
||||
title: title,
|
||||
adId: adId,
|
||||
trackingId: trackingId,
|
||||
clickId: value,
|
||||
openMode: openMode,
|
||||
entryPromoEnabled: entryPromoEnabled,
|
||||
entryBadgeText: entryBadgeText,
|
||||
entryTipTitle: entryTipTitle,
|
||||
entryTipBody: entryTipBody,
|
||||
quoteCtaEnabled: quoteCtaEnabled,
|
||||
entryRequest: request,
|
||||
allowedHosts: allowedHosts,
|
||||
);
|
||||
|
||||
static bool _isAllowed(Uri? uri) {
|
||||
if (uri == null || uri.host.isEmpty) return false;
|
||||
if (uri.scheme == 'https') return true;
|
||||
if (uri.scheme.toLowerCase() == 'https') return true;
|
||||
return !kReleaseMode &&
|
||||
uri.scheme == 'http' &&
|
||||
const {'localhost', '127.0.0.1', '::1'}.contains(uri.host);
|
||||
uri.scheme.toLowerCase() == 'http' &&
|
||||
const {
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
}.contains(uri.host.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
||||
MallTabConfigController(this._dio) : super(const MallTabConfig.disabled()) {
|
||||
refresh();
|
||||
MallTabConfigController(this._dio, {bool autoRefresh = true})
|
||||
: super(const MallTabConfig.disabled()) {
|
||||
if (autoRefresh) refresh();
|
||||
}
|
||||
|
||||
final Dio _dio;
|
||||
@@ -138,38 +171,51 @@ class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
||||
queryParameters: const {'placement': 'mall_tab'},
|
||||
options: protobufOptions(),
|
||||
);
|
||||
state = parseMallTabConfigResponse(response.data ?? const []);
|
||||
state = parseMallTabConfigResponse(response.data ?? const <int>[]);
|
||||
if (!state.isVisible) return;
|
||||
await _report('ad_impression');
|
||||
await _report(state, 'ad_impression');
|
||||
} catch (_) {
|
||||
state = const MallTabConfig.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> recordClick() async {
|
||||
if (!state.isVisible) return null;
|
||||
final clickId = const Uuid().v4();
|
||||
Future<MallTabConfig> recordClick({
|
||||
MallEntryRequest request = MallEntryRequest.home,
|
||||
}) async {
|
||||
if (!state.isVisible) return state;
|
||||
final clicked = state.withClick(const Uuid().v4(), request: request);
|
||||
state = clicked;
|
||||
try {
|
||||
await _report('ad_click', clickId: clickId);
|
||||
await _report(clicked, 'ad_click');
|
||||
} catch (_) {
|
||||
// 归因上报失败不能阻断商城入口。
|
||||
}
|
||||
return clickId;
|
||||
return clicked;
|
||||
}
|
||||
|
||||
Future<void> _report(String event, {String clickId = ''}) =>
|
||||
_dio.post<List<int>>(
|
||||
'/api/v1/ads/events',
|
||||
data: _ProtoWriter()
|
||||
.string(1, event)
|
||||
.string(2, 'mall_tab')
|
||||
.string(3, state.adId)
|
||||
.string(4, state.trackingId)
|
||||
.string(5, clickId)
|
||||
.integer(6, DateTime.now().millisecondsSinceEpoch)
|
||||
.toBytes(),
|
||||
options: protobufOptions(),
|
||||
);
|
||||
Future<void> _report(MallTabConfig config, String event) async {
|
||||
final response = await _dio.post<List<int>>(
|
||||
'/api/v1/ads/events',
|
||||
data: pb.ReportAdEventRequest(
|
||||
eventName: event,
|
||||
placement: 'mall_tab',
|
||||
adId: config.adId,
|
||||
trackingId: config.trackingId,
|
||||
clickId: config.clickId,
|
||||
clientTsMs: Int64(DateTime.now().millisecondsSinceEpoch),
|
||||
attrs: <String, String>{
|
||||
'source_app': config.from,
|
||||
'source_entry': config.entryRequest.sourceEntry,
|
||||
'hook_type': config.entryRequest.hookType,
|
||||
'entry_scene': ?config.entryRequest.entryScene,
|
||||
'entry_metal': ?config.entryRequest.entryMetal,
|
||||
'open_mode': config.openMode,
|
||||
},
|
||||
).writeToBuffer(),
|
||||
options: protobufOptions(),
|
||||
);
|
||||
pb.ReportAdEventReply.fromBuffer(response.data ?? const <int>[]);
|
||||
}
|
||||
}
|
||||
|
||||
final mallTabConfigProvider =
|
||||
@@ -177,52 +223,21 @@ 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 MallTabConfig config,
|
||||
required bool logged,
|
||||
required bool isDark,
|
||||
Uri? currentUri,
|
||||
bool isDark = false,
|
||||
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',
|
||||
...?currentUri?.queryParameters,
|
||||
...?config.attributedUri(isDark: isDark)?.queryParameters,
|
||||
'from': config.from,
|
||||
'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;
|
||||
@@ -230,119 +245,52 @@ Map<String, dynamic> buildEnterWebMallBridgeArguments({
|
||||
}
|
||||
final resolvedScene = scene?.trim() ?? '';
|
||||
if (resolvedScene.isNotEmpty) arguments['scene'] = resolvedScene;
|
||||
final resolvedSourceApp = sourceApp?.trim() ?? '';
|
||||
if (resolvedSourceApp.isNotEmpty) {
|
||||
arguments['from'] = resolvedSourceApp;
|
||||
arguments['source_app'] = resolvedSourceApp;
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
MallTabConfig parseMallTabConfigResponse(List<int> bytes) {
|
||||
final reply = pb.GetAdReply.fromBuffer(bytes);
|
||||
if (!reply.enabled || !reply.hasAd()) {
|
||||
return const MallTabConfig.disabled();
|
||||
}
|
||||
final creative = reply.ad;
|
||||
final url = _firstAllowedUrl(creative.trackedUrl, creative.h5Url);
|
||||
if (url == null) return const MallTabConfig.disabled();
|
||||
final promotion = creative.hasMallPromotion() ? creative.mallPromotion : null;
|
||||
return MallTabConfig(
|
||||
enabled: true,
|
||||
url: url,
|
||||
title: _nonEmpty(creative.title, '商城'),
|
||||
adId: creative.adId,
|
||||
trackingId: creative.trackingId,
|
||||
openMode: _openMode(creative.openMode),
|
||||
entryPromoEnabled: creative.entryPromoEnabled,
|
||||
entryBadgeText: creative.entryBadgeText.trim(),
|
||||
entryTipTitle: creative.entryTipTitle.trim(),
|
||||
entryTipBody: creative.entryTipBody.trim(),
|
||||
quoteCtaEnabled: promotion?.quoteCtaEnabled ?? false,
|
||||
allowedHosts: <String>{Uri.parse(url).host.toLowerCase()},
|
||||
);
|
||||
}
|
||||
|
||||
String _nonEmpty(String value, String fallback) =>
|
||||
value.trim().isEmpty ? fallback : value.trim();
|
||||
|
||||
String? _validUrl(String raw) {
|
||||
final uri = Uri.tryParse(raw.trim());
|
||||
return MallTabConfig._isAllowed(uri) ? uri.toString() : null;
|
||||
}
|
||||
|
||||
String _openMode(int value) => switch (value) {
|
||||
1 => 'native_app',
|
||||
2 => 'in_app',
|
||||
String _openMode(pb.AdOpenMode value) => switch (value) {
|
||||
pb.AdOpenMode.AD_OPEN_MODE_NATIVE_APP => 'native_app',
|
||||
pb.AdOpenMode.AD_OPEN_MODE_IN_APP => 'in_app',
|
||||
_ => 'external',
|
||||
};
|
||||
|
||||
/// Parses the protobuf reply returned by `/api/v1/ads?placement=mall_tab`.
|
||||
MallTabConfig parseMallTabConfigResponse(List<int> bytes) {
|
||||
final response = _ProtoMessage(bytes);
|
||||
final reply = response.message(2);
|
||||
final trackedUrl = reply.string(14);
|
||||
final h5Url = reply.string(9);
|
||||
final url = _validUrl(trackedUrl) ?? _validUrl(h5Url);
|
||||
if (response.integer(1) == 0 || url == null) {
|
||||
return const MallTabConfig.disabled();
|
||||
}
|
||||
return MallTabConfig(
|
||||
enabled: true,
|
||||
url: url,
|
||||
title: _nonEmpty(reply.string(4), '商城'),
|
||||
adId: reply.string(1),
|
||||
trackingId: reply.string(10),
|
||||
openMode: _openMode(reply.integer(15)),
|
||||
entryPromoEnabled: reply.integer(16) != 0,
|
||||
entryBadgeText: reply.string(17).trim(),
|
||||
entryTipTitle: reply.string(18).trim(),
|
||||
entryTipBody: reply.string(19).trim(),
|
||||
);
|
||||
}
|
||||
|
||||
class _ProtoMessage {
|
||||
_ProtoMessage(this._bytes);
|
||||
final List<int> _bytes;
|
||||
|
||||
int integer(int wanted) =>
|
||||
_fields()[wanted] is int ? _fields()[wanted]! as int : 0;
|
||||
String string(int wanted) => _fields()[wanted] is 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 [],
|
||||
);
|
||||
|
||||
Map<int, Object> _fields() {
|
||||
final values = <int, Object>{};
|
||||
var index = 0;
|
||||
int readVarint() {
|
||||
var value = 0;
|
||||
var shift = 0;
|
||||
while (index < _bytes.length && shift < 64) {
|
||||
final byte = _bytes[index++];
|
||||
value |= (byte & 127) << shift;
|
||||
if (byte < 128) return value;
|
||||
shift += 7;
|
||||
}
|
||||
throw const FormatException('Invalid protobuf');
|
||||
}
|
||||
|
||||
while (index < _bytes.length) {
|
||||
final key = readVarint();
|
||||
final tag = key >> 3;
|
||||
switch (key & 7) {
|
||||
case 0:
|
||||
values[tag] = readVarint();
|
||||
case 2:
|
||||
final length = readVarint();
|
||||
final end = index + length;
|
||||
if (end > _bytes.length) {
|
||||
throw const FormatException('Invalid protobuf');
|
||||
}
|
||||
values[tag] = _bytes.sublist(index, end);
|
||||
index = end;
|
||||
default:
|
||||
throw const FormatException('Unsupported protobuf');
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
class _ProtoWriter {
|
||||
final _bytes = BytesBuilder(copy: false);
|
||||
_ProtoWriter string(int tag, String value) {
|
||||
final data = value.codeUnits;
|
||||
_varint((tag << 3) | 2);
|
||||
_varint(data.length);
|
||||
_bytes.add(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
_ProtoWriter integer(int tag, int value) {
|
||||
_varint(tag << 3);
|
||||
_varint(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
Uint8List toBytes() => _bytes.toBytes();
|
||||
void _varint(int value) {
|
||||
while (value > 127) {
|
||||
_bytes.addByte((value & 127) | 128);
|
||||
value >>= 7;
|
||||
}
|
||||
_bytes.addByte(value);
|
||||
String? _firstAllowedUrl(String trackedUrl, String h5Url) {
|
||||
for (final raw in <String>[trackedUrl, h5Url]) {
|
||||
final uri = Uri.tryParse(raw.trim());
|
||||
if (MallTabConfig._isAllowed(uri)) return uri.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user