fix:对齐金值,主要注销,一键登录等方面修改
This commit is contained in:
1 parent
e29ed0a2c8
commit
6997a65c96
37 files changed
+3435
-507
No files matched your search
Binary file not shown.
@@ -5,6 +5,7 @@ class AppConfig {
|
|||||||
const url = String.fromEnvironment(
|
const url = String.fromEnvironment(
|
||||||
'JINZHI_API_BASE_URL',
|
'JINZHI_API_BASE_URL',
|
||||||
// defaultValue: 'http://localhost:8088',
|
// defaultValue: 'http://localhost:8088',
|
||||||
|
// defaultValue: 'https://jince.api.xianlin88.top',
|
||||||
defaultValue: 'https://jinzhi.api.yqzhice.top',
|
defaultValue: 'https://jinzhi.api.yqzhice.top',
|
||||||
|
|
||||||
// defaultValue: 'http://8.153.17.57',可测试ip
|
// defaultValue: 'http://8.153.17.57',可测试ip
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class AppH5Urls {
|
|||||||
|
|
||||||
static const baseUrl = String.fromEnvironment(
|
static const baseUrl = String.fromEnvironment(
|
||||||
'JINZHI_H5_BASE_URL',
|
'JINZHI_H5_BASE_URL',
|
||||||
defaultValue: 'https://yanting.h5.hnsiderui.cn',
|
defaultValue: 'https://jinzhi.h5.yqzhice.top',
|
||||||
);
|
);
|
||||||
|
|
||||||
static String get privacyUrl => '$baseUrl/beginner/privacyPolicy.html';
|
static String get privacyUrl => '$baseUrl/beginner/privacyPolicy.html';
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ class UmengConfig {
|
|||||||
static String _androidAppKey =
|
static String _androidAppKey =
|
||||||
'6a27b1f46f259537c7b616be'; // Default placeholder
|
'6a27b1f46f259537c7b616be'; // Default placeholder
|
||||||
static String _iosAppKey = '6a27b24e6f259537c7b617a8'; // Default placeholder
|
static String _iosAppKey = '6a27b24e6f259537c7b617a8'; // Default placeholder
|
||||||
static String _androidPushSecret = 'xxx'; // Default placeholder
|
static String _androidPushSecret =
|
||||||
static String _androidLoginSecret = '';
|
'261fc7656f0c34f4cf122a3c2c81f704'; // Default placeholder
|
||||||
static String _iosLoginSecret = '';
|
static String _androidLoginSecret =
|
||||||
|
'oNaEhNEwZGQHvngXI4kyxR90tavJRDHMyDDQhkQouty+SAUb66szdNQivqE99ZIQO+QyNabsyyTJ7CN8iA3vb8EgPwLF/iSUHMWpGVacHpukG1sqySo1cwbRmDpOFcdNzaO8PavGfqkkCse/oRPNFBW40LhCjSJaI/roAEVNGpepMXvufJtjnyB4OX6sav+isHU/Lsz84bcdwnzD6fFyDSyE+YqN9gjUoUOv8Ghcy3H1FXUMvw4iD9nD+tE4jODLXvvzlpVbHYknn6Y2UAj5dvGvNUszocXqGAr4j3z8naKPBhz7Liofog==';
|
||||||
|
static String _iosLoginSecret =
|
||||||
|
'vcutZmDFJ2HBdFk7herCz2M9dPHuoa303NPGrm8c9gkzu/Lr5njFa85y33w7htgp0cx9JCMbsk3ecrREhVd8yeq73kFFiVqgkk/BMzCQ6VG55gSATEoiRaLnqXkKvuv6X4kcnSqANMI07up5rTPdAtVQ5JGPh9ts7LUBzRZWQRnDvdX8pjPy8LKxXJKoNagz4sHd5m9MbX0o0uep2OaFFFBTOyI+g4vjK1oZkpaUnIKGU8cTH+wvL8qwp5BLtkc6XCXe5PXqClU=';
|
||||||
|
|
||||||
static Future<void> load() async {
|
static Future<void> load() async {
|
||||||
await dotenv.load(
|
await dotenv.load(
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import '../../models/pb/errors/errors.pb.dart' as kerrors;
|
||||||
|
|
||||||
class KratosErrorInterceptor extends Interceptor {
|
class KratosErrorInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
final response = err.response;
|
||||||
|
final raw = response?.data;
|
||||||
|
if (response != null && raw is List<int> && raw.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final status = kerrors.Status.fromBuffer(raw);
|
||||||
|
if (status.code != 0 || status.reason.isNotEmpty) {
|
||||||
|
response.data = <String, dynamic>{
|
||||||
|
'code': status.code,
|
||||||
|
'reason': status.reason,
|
||||||
|
'message': status.message,
|
||||||
|
'metadata': status.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// A non-Kratos error body keeps its original representation.
|
||||||
|
}
|
||||||
|
}
|
||||||
handler.next(err);
|
handler.next(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,9 @@ const storageJinzhiAmountHidden = 'jinzhi_amount_hidden';
|
|||||||
const storageJinzhiRecycleDiscounts = 'jinzhi_recycle_discounts';
|
const storageJinzhiRecycleDiscounts = 'jinzhi_recycle_discounts';
|
||||||
const storageJinzhiPurityPresets = 'jinzhi_purity_presets';
|
const storageJinzhiPurityPresets = 'jinzhi_purity_presets';
|
||||||
const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode';
|
const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode';
|
||||||
const storageJinzhiMockLoggedIn = 'jinzhi_mock_logged_in';
|
const storageUserId = 'jinzhi_user_id';
|
||||||
|
const storageUserPhone = 'jinzhi_user_phone';
|
||||||
|
const storageUserCreatedAt = 'jinzhi_user_created_at';
|
||||||
const storagePushPermissionPrompted = 'push_permission_prompted';
|
const storagePushPermissionPrompted = 'push_permission_prompted';
|
||||||
const storageMallEntryPromoOpened = 'jinzhi_mall_entry_promo_opened';
|
const storageMallEntryPromoOpened = 'jinzhi_mall_entry_promo_opened';
|
||||||
const storageMallHomeBadgeSeen = 'jinzhi_mall_home_badge_seen';
|
const storageMallHomeBadgeSeen = 'jinzhi_mall_home_badge_seen';
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ abstract final class UmengService {
|
|||||||
// Keep the carrier-login launch path independent of UMeng Common's
|
// Keep the carrier-login launch path independent of UMeng Common's
|
||||||
// network initialization. Waiting for this future is what left the login
|
// network initialization. Waiting for this future is what left the login
|
||||||
// overlay spinning forever in the base project; U-Verify is configured
|
// overlay spinning forever in the base project; U-Verify is configured
|
||||||
// immediately below by initOneKeyLoginIfAllowed, as in Yanting.
|
// immediately below by initOneKeyLoginIfAllowed.
|
||||||
unawaited(
|
unawaited(
|
||||||
UmengCommonSdk.initCommon(
|
UmengCommonSdk.initCommon(
|
||||||
UmengConfig.androidAppKey,
|
UmengConfig.androidAppKey,
|
||||||
|
|||||||
@@ -1,92 +1,143 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../../common/network/auth_interceptor.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
|
import '../../../common/storage/secure_identity_storage.dart';
|
||||||
import '../../../common/storage/storage_keys.dart';
|
import '../../../common/storage/storage_keys.dart';
|
||||||
import '../../../common/storage/storage_service.dart';
|
import '../../../common/storage/storage_service.dart';
|
||||||
import '../../../common/network/dio_provider.dart';
|
|
||||||
import '../data/auth_models.dart';
|
|
||||||
import '../data/auth_api.dart';
|
import '../data/auth_api.dart';
|
||||||
import '../data/one_key_login_api.dart';
|
import '../data/auth_models.dart';
|
||||||
|
|
||||||
|
const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in';
|
||||||
|
|
||||||
final authProvider =
|
final authProvider =
|
||||||
StateNotifierProvider<AuthController, AsyncValue<AuthState>>(
|
StateNotifierProvider<AuthController, AsyncValue<AuthState>>((ref) {
|
||||||
(ref) => AuthController(ref.read(dioProvider)),
|
return AuthController(
|
||||||
|
ref.read(dioProvider),
|
||||||
|
identityStorage: ref.read(secureIdentityStorageProvider),
|
||||||
|
preferences: StorageService.to.preferences,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
||||||
AuthController(this._dio) : super(const AsyncLoading()) {
|
AuthController(
|
||||||
_bootstrap();
|
this._dio, {
|
||||||
|
required SecureIdentityStorage identityStorage,
|
||||||
|
required SharedPreferences preferences,
|
||||||
|
bool autoBootstrap = true,
|
||||||
|
}) : _identityStorage = identityStorage,
|
||||||
|
_preferences = preferences,
|
||||||
|
super(const AsyncLoading()) {
|
||||||
|
if (autoBootstrap) _bootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
final SecureIdentityStorage _identityStorage;
|
||||||
|
final SharedPreferences _preferences;
|
||||||
|
|
||||||
|
AuthApi get _api => AuthApi(_dio);
|
||||||
|
|
||||||
|
Future<void> retryBootstrap() => _bootstrap();
|
||||||
|
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
final token = await loadJwt();
|
state = const AsyncLoading();
|
||||||
|
final token = await _identityStorage.readJwt();
|
||||||
if (token == null || token.isEmpty) {
|
if (token == null || token.isEmpty) {
|
||||||
state = const AsyncData(GuestAuthState());
|
state = const AsyncData(GuestAuthState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final phone = StorageService.to.getString(storageJinzhiMockLoggedIn) ?? '';
|
try {
|
||||||
state = AsyncData(
|
final user = await _api.getProfile();
|
||||||
LoggedInAuthState(
|
await _persistProfile(user);
|
||||||
user: JinzhiUser(
|
state = AsyncData(LoggedInAuthState(user: user, token: token));
|
||||||
phone: phone.isEmpty ? '13800000000' : phone,
|
} catch (error, stackTrace) {
|
||||||
nickname: '金值用户',
|
if (_isUnauthorized(error)) {
|
||||||
createdAt: DateTime(2026, 6, 18),
|
await _clearLocalSession();
|
||||||
),
|
state = const AsyncData(GuestAuthState());
|
||||||
token: token,
|
return;
|
||||||
),
|
}
|
||||||
);
|
final cached = _canUseCachedProfile(error) ? _readCachedProfile() : null;
|
||||||
|
if (cached != null) {
|
||||||
|
await _persistProfile(cached);
|
||||||
|
state = AsyncData(LoggedInAuthState(user: cached, token: token));
|
||||||
|
} else {
|
||||||
|
state = AsyncError(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendCode(String phone) async {
|
Future<void> sendCode(String phone) => _api.sendSmsCode(phone);
|
||||||
await AuthApi(_dio).sendSmsCode(phone);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> verifyCode(String phone, String code) async {
|
Future<void> verifyCode(String phone, String code) async {
|
||||||
final reply = await AuthApi(_dio).verifySmsCode(phone, code);
|
final reply = await _api.verifySmsCode(phone, code);
|
||||||
await saveJwt(reply.accessToken);
|
await _persistLogin(reply);
|
||||||
await StorageService.to.setString(storageJinzhiMockLoggedIn, reply.phone);
|
|
||||||
state = AsyncData(
|
|
||||||
LoggedInAuthState(
|
|
||||||
user: JinzhiUser(
|
|
||||||
phone: reply.phone,
|
|
||||||
nickname: '金值用户',
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
token: reply.accessToken,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test environment reuses Yanting's one-key verification endpoint until
|
|
||||||
/// 金值 has its own account contract.
|
|
||||||
Future<void> verifyOneKeyLogin(String loginToken) async {
|
Future<void> verifyOneKeyLogin(String loginToken) async {
|
||||||
final reply = await OneKeyLoginApi(_dio).verify(loginToken);
|
final reply = await _api.verifyOneKeyLogin(loginToken);
|
||||||
await saveJwt(reply.token);
|
await _persistLogin(reply);
|
||||||
await StorageService.to.setString(storageJinzhiMockLoggedIn, reply.phone);
|
}
|
||||||
|
|
||||||
|
Future<void> _persistLogin(AuthLoginResult reply) async {
|
||||||
|
await _identityStorage.saveJwt(reply.accessToken);
|
||||||
|
await _persistProfile(reply.user);
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
LoggedInAuthState(
|
LoggedInAuthState(user: reply.user, token: reply.accessToken),
|
||||||
user: JinzhiUser(
|
|
||||||
phone: reply.phone,
|
|
||||||
nickname: '金值用户',
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
token: reply.token,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await clearJwt();
|
await _clearLocalSession();
|
||||||
await StorageService.to.setString(storageJinzhiMockLoggedIn, '');
|
|
||||||
state = const AsyncData(GuestAuthState());
|
state = const AsyncData(GuestAuthState());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAccount([String? code]) async {
|
Future<void> deleteAccount(String code) async {
|
||||||
await logout();
|
final normalized = code.trim();
|
||||||
|
if (normalized.isEmpty) {
|
||||||
|
throw ArgumentError.value(code, 'code', 'must not be empty');
|
||||||
|
}
|
||||||
|
await _api.deleteAccount(normalized);
|
||||||
|
await _clearLocalSession();
|
||||||
|
state = const AsyncData(GuestAuthState());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistProfile(JinzhiUser user) async {
|
||||||
|
await _preferences.setString(storageUserId, user.id);
|
||||||
|
await _preferences.setString(storageUserPhone, user.phone);
|
||||||
|
await _preferences.setString(storageUserCreatedAt, user.createdAt);
|
||||||
|
await _preferences.remove(_legacyMockLoggedInKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
JinzhiUser? _readCachedProfile() {
|
||||||
|
final id = _preferences.getString(storageUserId)?.trim() ?? '';
|
||||||
|
final phone =
|
||||||
|
(_preferences.getString(storageUserPhone) ??
|
||||||
|
_preferences.getString(_legacyMockLoggedInKey) ??
|
||||||
|
'')
|
||||||
|
.trim();
|
||||||
|
final createdAt =
|
||||||
|
_preferences.getString(storageUserCreatedAt)?.trim() ?? '';
|
||||||
|
if (id.isEmpty && phone.isEmpty) return null;
|
||||||
|
return JinzhiUser(id: id, phone: phone, createdAt: createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearLocalSession() async {
|
||||||
|
await _identityStorage.clearJwt();
|
||||||
|
await _preferences.remove(storageUserId);
|
||||||
|
await _preferences.remove(storageUserPhone);
|
||||||
|
await _preferences.remove(storageUserCreatedAt);
|
||||||
|
await _preferences.remove(_legacyMockLoggedInKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isLoggedIn => state.valueOrNull is LoggedInAuthState;
|
bool get isLoggedIn => state.valueOrNull is LoggedInAuthState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isUnauthorized(Object error) =>
|
||||||
|
error is DioException && error.response?.statusCode == 401;
|
||||||
|
|
||||||
|
bool _canUseCachedProfile(Object error) {
|
||||||
|
if (error is! DioException) return false;
|
||||||
|
final status = error.response?.statusCode;
|
||||||
|
return status == null || status == 408 || status == 429 || status >= 500;
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ abstract interface class OneKeyLoginGateway {
|
|||||||
|
|
||||||
void setWidgetCallback(OneKeyLoginCallback? callback);
|
void setWidgetCallback(OneKeyLoginCallback? callback);
|
||||||
|
|
||||||
|
/// 仅在用户点按一键登录后调用,预热运营商授权页。
|
||||||
|
Future<Object?> accelerate();
|
||||||
|
|
||||||
Future<void> requestLogin(UMCustomModel uiConfig);
|
Future<void> requestLogin(UMCustomModel uiConfig);
|
||||||
|
|
||||||
void closeAuthPage();
|
void closeAuthPage();
|
||||||
@@ -65,6 +68,11 @@ class UmengOneKeyLoginGateway implements OneKeyLoginGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Object?> accelerate() {
|
||||||
|
return UmengVerifySdk.accelerateLoginPageWithTimeout(3);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> requestLogin(UMCustomModel uiConfig) async {
|
Future<void> requestLogin(UMCustomModel uiConfig) async {
|
||||||
await UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig);
|
await UmengVerifySdk.getLoginTokenWithTimeout(3, uiConfig);
|
||||||
@@ -137,15 +145,25 @@ class OneKeyLoginSdkSession {
|
|||||||
return _result.future;
|
return _result.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Availability checking already pre-accelerates the carrier page. Start
|
// 预热必须由用户显式点击触发;不在启动或隐私同意阶段调用它。
|
||||||
// the token request directly here; the SDK performs its own preflight when
|
|
||||||
// needed and reports its timeout/error through the registered callback.
|
|
||||||
unawaited(_startNativeLogin(uiConfig));
|
unawaited(_startNativeLogin(uiConfig));
|
||||||
return _result.future;
|
return _result.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startNativeLogin(UMCustomModel uiConfig) async {
|
Future<void> _startNativeLogin(UMCustomModel uiConfig) async {
|
||||||
try {
|
try {
|
||||||
|
// 与 jinzhi-flutter-app 保持一致。未预热时 SDK 会在此处隐式预热,
|
||||||
|
// 在部分 Android 机型上更容易超时或直接回调失败。
|
||||||
|
final preheat = await _gateway.accelerate();
|
||||||
|
if (kDebugMode && preheat is Map && preheat['ret'] != null) {
|
||||||
|
// `ret` 是运营商预热失败码;插件原生层未将它输出到 logcat。
|
||||||
|
// 预热失败不阻断接下来的 getLoginToken,SDK 会自行再试一次。
|
||||||
|
debugPrint(
|
||||||
|
'OneKeyLogin preheat failed: vendor=${preheat['vendor']}, '
|
||||||
|
'ret=${preheat['ret']}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_terminal) return;
|
||||||
_gateway.setTokenCallback(_handleTokenResult);
|
_gateway.setTokenCallback(_handleTokenResult);
|
||||||
_gateway.setWidgetCallback((Object? _) {});
|
_gateway.setWidgetCallback((Object? _) {});
|
||||||
_gateway.setUiCallback(_handleUiResult);
|
_gateway.setUiCallback(_handleUiResult);
|
||||||
@@ -186,6 +204,15 @@ class OneKeyLoginSdkSession {
|
|||||||
|
|
||||||
void _handleTokenResult(Object? result) {
|
void _handleTokenResult(Object? result) {
|
||||||
final code = extractUmengLoginCode(result);
|
final code = extractUmengLoginCode(result);
|
||||||
|
if (kDebugMode && !isUmengTokenSuccess(result)) {
|
||||||
|
final map = result is Map ? result : const <Object?, Object?>{};
|
||||||
|
// 本地插件原先丢弃了 onTokenFailed 的 ret;只记录诊断字段,绝不记录 token。
|
||||||
|
debugPrint(
|
||||||
|
'OneKeyLogin Android/iOS SDK result: code=$code, '
|
||||||
|
'msg=${map['msg']}, vendor=${map['vendorName']}, '
|
||||||
|
'carrierError=${map['carrierFailedResultData']}',
|
||||||
|
);
|
||||||
|
}
|
||||||
// On iOS, 700002 only means the user tapped the authorization page's
|
// On iOS, 700002 only means the user tapped the authorization page's
|
||||||
// login button. It is followed by a separate 600000 (or 600xxx) token
|
// login button. It is followed by a separate 600000 (or 600xxx) token
|
||||||
// callback, so completing the session here discards that callback.
|
// callback, so completing the session here discards that callback.
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../../../common/network/dio_provider.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
|
import '../../../models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||||
|
import 'auth_models.dart';
|
||||||
|
|
||||||
/// 金值账号服务的 protobuf HTTP 契约。
|
/// 金值账户的 protobuf-over-HTTP 客户端。
|
||||||
///
|
|
||||||
/// 这个轻量编解码只覆盖短信登录需要的字段,避免把整套历史生成模型带入
|
|
||||||
/// 当前重建中的应用;字段号与金值 `account.proto` 保持一致。
|
|
||||||
class AuthApi {
|
class AuthApi {
|
||||||
const AuthApi(this._dio);
|
const AuthApi(this._dio);
|
||||||
|
|
||||||
@@ -17,124 +13,82 @@ class AuthApi {
|
|||||||
Future<int> sendSmsCode(String phone) async {
|
Future<int> sendSmsCode(String phone) async {
|
||||||
final response = await _dio.post<List<int>>(
|
final response = await _dio.post<List<int>>(
|
||||||
'/api/v1/auth/sms/send',
|
'/api/v1/auth/sms/send',
|
||||||
// Dio may JSON-encode a List<int> as `[10,11,...]`. The API expects
|
data: pb.SendSmsCodeRequest(phone: phone).writeToBuffer(),
|
||||||
// the raw protobuf bytes, so preserve the binary payload explicitly.
|
|
||||||
data: Uint8List.fromList(_ProtoWriter.stringField(1, phone)),
|
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
return _ProtoMessage(response.data ?? const <int>[]).integer(1);
|
return pb.SendSmsCodeReply.fromBuffer(
|
||||||
|
response.data ?? const <int>[],
|
||||||
|
).expiresIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthLoginResult> verifySmsCode(String phone, String code) async {
|
Future<AuthLoginResult> verifySmsCode(String phone, String code) async {
|
||||||
final response = await _dio.post<List<int>>(
|
final response = await _dio.post<List<int>>(
|
||||||
'/api/v1/auth/sms/verify',
|
'/api/v1/auth/sms/verify',
|
||||||
data: Uint8List.fromList([
|
data: pb.VerifySmsCodeRequest(phone: phone, code: code).writeToBuffer(),
|
||||||
..._ProtoWriter.stringField(1, phone),
|
|
||||||
..._ProtoWriter.stringField(2, code),
|
|
||||||
]),
|
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
final reply = _ProtoMessage(response.data ?? const <int>[]);
|
return AuthLoginResult.fromProto(
|
||||||
final accessToken = reply.string(1).trim();
|
pb.VerifySmsCodeReply.fromBuffer(response.data ?? const <int>[]),
|
||||||
if (accessToken.isEmpty) {
|
);
|
||||||
throw const FormatException('登录响应缺少 access_token');
|
|
||||||
}
|
}
|
||||||
final user = _ProtoMessage(reply.bytes(3));
|
|
||||||
return AuthLoginResult(
|
Future<AuthLoginResult> verifyOneKeyLogin(String loginToken) async {
|
||||||
accessToken: accessToken,
|
final response = await _dio.post<List<int>>(
|
||||||
phone: user.string(2).trim().isEmpty ? phone : user.string(2).trim(),
|
'/api/v1/auth/onekey/verify',
|
||||||
|
data: pb.VerifyOneKeyLoginRequest(loginToken: loginToken).writeToBuffer(),
|
||||||
|
options: protobufOptions(),
|
||||||
|
);
|
||||||
|
return AuthLoginResult.fromProto(
|
||||||
|
pb.VerifySmsCodeReply.fromBuffer(response.data ?? const <int>[]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JinzhiUser> getProfile() async {
|
||||||
|
final response = await _dio.get<List<int>>(
|
||||||
|
'/api/v1/me',
|
||||||
|
options: protobufOptions(),
|
||||||
|
);
|
||||||
|
final reply = pb.ProfileReply.fromBuffer(response.data ?? const <int>[]);
|
||||||
|
if (!reply.hasUser()) {
|
||||||
|
throw const FormatException('账号资料响应缺少 user');
|
||||||
|
}
|
||||||
|
return _userFromProto(reply.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAccount(String smsCode) async {
|
||||||
|
await _dio.delete<List<int>>(
|
||||||
|
'/api/v1/me',
|
||||||
|
data: pb.DeleteAccountRequest(smsCode: smsCode).writeToBuffer(),
|
||||||
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthLoginResult {
|
class AuthLoginResult {
|
||||||
const AuthLoginResult({required this.accessToken, required this.phone});
|
const AuthLoginResult({
|
||||||
|
required this.accessToken,
|
||||||
|
required this.expiresIn,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
final String accessToken;
|
final String accessToken;
|
||||||
final String phone;
|
final int expiresIn;
|
||||||
|
final JinzhiUser user;
|
||||||
|
|
||||||
|
factory AuthLoginResult.fromProto(pb.VerifySmsCodeReply reply) {
|
||||||
|
if (reply.accessToken.trim().isEmpty || !reply.hasUser()) {
|
||||||
|
throw const FormatException('登录响应缺少 access_token 或 user');
|
||||||
}
|
}
|
||||||
|
return AuthLoginResult(
|
||||||
class _ProtoWriter {
|
accessToken: reply.accessToken.trim(),
|
||||||
static List<int> stringField(int field, String value) => [
|
expiresIn: reply.expiresIn,
|
||||||
..._varint(field << 3 | 2),
|
user: _userFromProto(reply.user),
|
||||||
..._varint(utf8.encode(value).length),
|
|
||||||
...utf8.encode(value),
|
|
||||||
];
|
|
||||||
|
|
||||||
static List<int> _varint(int value) {
|
|
||||||
final bytes = <int>[];
|
|
||||||
var remaining = value;
|
|
||||||
while (remaining > 0x7f) {
|
|
||||||
bytes.add(remaining & 0x7f | 0x80);
|
|
||||||
remaining >>= 7;
|
|
||||||
}
|
|
||||||
bytes.add(remaining);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProtoMessage {
|
|
||||||
_ProtoMessage(List<int> bytes) : _fields = _parse(bytes);
|
|
||||||
|
|
||||||
final Map<int, Object> _fields;
|
|
||||||
|
|
||||||
int integer(int field) => _fields[field] is int ? _fields[field]! as int : 0;
|
|
||||||
|
|
||||||
String string(int field) => _fields[field] is List<int>
|
|
||||||
? utf8.decode(_fields[field]! as List<int>, allowMalformed: true)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
List<int> bytes(int field) => _fields[field] is List<int>
|
|
||||||
? _fields[field]! as List<int>
|
|
||||||
: const <int>[];
|
|
||||||
|
|
||||||
static Map<int, Object> _parse(List<int> bytes) {
|
|
||||||
final fields = <int, Object>{};
|
|
||||||
var cursor = 0;
|
|
||||||
while (cursor < bytes.length) {
|
|
||||||
final tag = _readVarint(bytes, cursor);
|
|
||||||
if (tag == null) break;
|
|
||||||
cursor = tag.next;
|
|
||||||
final field = tag.value >> 3;
|
|
||||||
switch (tag.value & 7) {
|
|
||||||
case 0:
|
|
||||||
final value = _readVarint(bytes, cursor);
|
|
||||||
if (value == null) return fields;
|
|
||||||
fields[field] = value.value;
|
|
||||||
cursor = value.next;
|
|
||||||
case 2:
|
|
||||||
final length = _readVarint(bytes, cursor);
|
|
||||||
if (length == null || length.next + length.value > bytes.length) {
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
fields[field] = bytes.sublist(
|
|
||||||
length.next,
|
|
||||||
length.next + length.value,
|
|
||||||
);
|
);
|
||||||
cursor = length.next + length.value;
|
|
||||||
default:
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static _ProtoValue? _readVarint(List<int> bytes, int cursor) {
|
|
||||||
var value = 0;
|
|
||||||
var shift = 0;
|
|
||||||
while (cursor < bytes.length && shift <= 63) {
|
|
||||||
final byte = bytes[cursor++];
|
|
||||||
value |= (byte & 0x7f) << shift;
|
|
||||||
if (byte & 0x80 == 0) return _ProtoValue(value, cursor);
|
|
||||||
shift += 7;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProtoValue {
|
JinzhiUser _userFromProto(pb.UserInfo user) => JinzhiUser(
|
||||||
const _ProtoValue(this.value, this.next);
|
id: user.id.trim(),
|
||||||
|
phone: user.phone.trim(),
|
||||||
final int value;
|
createdAt: user.createdAt.trim(),
|
||||||
final int next;
|
);
|
||||||
}
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
class JinzhiUser {
|
class JinzhiUser {
|
||||||
const JinzhiUser({
|
const JinzhiUser({
|
||||||
|
required this.id,
|
||||||
required this.phone,
|
required this.phone,
|
||||||
required this.nickname,
|
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
final String phone;
|
final String phone;
|
||||||
final String nickname;
|
final String createdAt;
|
||||||
final DateTime createdAt;
|
|
||||||
|
String get nickname => '金值用户';
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class AuthState {
|
sealed class AuthState {
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
|
|
||||||
import '../../../common/network/dio_provider.dart';
|
|
||||||
|
|
||||||
/// Compatible with Yanting's protobuf `/api/v1/auth/onekey/verify` contract.
|
|
||||||
class OneKeyLoginApi {
|
|
||||||
OneKeyLoginApi(this._dio);
|
|
||||||
final Dio _dio;
|
|
||||||
|
|
||||||
Future<OneKeyLoginReply> verify(String loginToken) async {
|
|
||||||
final bytes = utf8.encode(loginToken);
|
|
||||||
final request = Uint8List.fromList([
|
|
||||||
0x0A,
|
|
||||||
..._varint(bytes.length),
|
|
||||||
...bytes,
|
|
||||||
]);
|
|
||||||
final response = await _dio.post<List<int>>(
|
|
||||||
'/api/v1/auth/onekey/verify',
|
|
||||||
data: request,
|
|
||||||
options: protobufOptions(),
|
|
||||||
);
|
|
||||||
return _decode(Uint8List.fromList(response.data ?? const []));
|
|
||||||
}
|
|
||||||
|
|
||||||
OneKeyLoginReply _decode(Uint8List data) {
|
|
||||||
final fields = _fields(data);
|
|
||||||
final token = utf8.decode(fields[1] ?? const []);
|
|
||||||
final user = _fields(Uint8List.fromList(fields[3] ?? const []));
|
|
||||||
if (token.isEmpty) throw StateError('一键登录服务未返回登录凭证');
|
|
||||||
return OneKeyLoginReply(
|
|
||||||
token: token,
|
|
||||||
phone: utf8.decode(user[2] ?? const []),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<int, List<int>> _fields(Uint8List data) {
|
|
||||||
final result = <int, List<int>>{};
|
|
||||||
var i = 0;
|
|
||||||
while (i < data.length) {
|
|
||||||
final tag = _readVarint(data, i);
|
|
||||||
i = tag.$2;
|
|
||||||
final wireType = tag.$1 & 7;
|
|
||||||
if (wireType == 0) {
|
|
||||||
i = _readVarint(data, i).$2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (wireType != 2) {
|
|
||||||
throw const FormatException('Unsupported protobuf field');
|
|
||||||
}
|
|
||||||
final length = _readVarint(data, i);
|
|
||||||
i = length.$2;
|
|
||||||
final end = i + length.$1;
|
|
||||||
if (end > data.length) {
|
|
||||||
throw const FormatException('Invalid protobuf payload');
|
|
||||||
}
|
|
||||||
result[tag.$1 >> 3] = data.sublist(i, end);
|
|
||||||
i = end;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
(int, int) _readVarint(Uint8List data, int index) {
|
|
||||||
var value = 0;
|
|
||||||
var shift = 0;
|
|
||||||
while (index < data.length) {
|
|
||||||
final byte = data[index++];
|
|
||||||
value |= (byte & 0x7f) << shift;
|
|
||||||
if (byte & 0x80 == 0) return (value, index);
|
|
||||||
shift += 7;
|
|
||||||
}
|
|
||||||
throw const FormatException('Invalid protobuf varint');
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> _varint(int value) {
|
|
||||||
final result = <int>[];
|
|
||||||
while (value > 0x7f) {
|
|
||||||
result.add((value & 0x7f) | 0x80);
|
|
||||||
value >>= 7;
|
|
||||||
}
|
|
||||||
result.add(value);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OneKeyLoginReply {
|
|
||||||
const OneKeyLoginReply({required this.token, required this.phone});
|
|
||||||
final String token;
|
|
||||||
final String phone;
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,20 @@ class DeleteAccountPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: cs.background,
|
backgroundColor: cs.background,
|
||||||
body: Center(child: Text('加载失败:$e')),
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('账号状态加载失败'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: () =>
|
||||||
|
ref.read(authProvider.notifier).retryBootstrap(),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
data: (authState) {
|
data: (authState) {
|
||||||
if (authState is! LoggedInAuthState) {
|
if (authState is! LoggedInAuthState) {
|
||||||
@@ -669,7 +682,9 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
|
|||||||
final cs = ShadTheme.of(context).colorScheme;
|
final cs = ShadTheme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
// 键盘弹出时 sheet 跟随上移
|
// 键盘弹出时 sheet 跟随上移
|
||||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||||
@@ -715,11 +730,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
|
|||||||
return [
|
return [
|
||||||
Text(
|
Text(
|
||||||
'正在清除账户数据,请稍候',
|
'正在清除账户数据,请稍候',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
|
||||||
fontSize: 13,
|
|
||||||
height: 1.55,
|
|
||||||
color: cs.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
Center(
|
Center(
|
||||||
@@ -737,11 +748,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
|
|||||||
return [
|
return [
|
||||||
Text(
|
Text(
|
||||||
'为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}。',
|
'为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}。',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
|
||||||
fontSize: 13,
|
|
||||||
height: 1.55,
|
|
||||||
color: cs.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (_error != null) ...[
|
if (_error != null) ...[
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -768,11 +775,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
|
|||||||
return [
|
return [
|
||||||
Text(
|
Text(
|
||||||
'验证码已发送至 ${_maskPhone(widget.phone)}',
|
'验证码已发送至 ${_maskPhone(widget.phone)}',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
|
||||||
fontSize: 13,
|
|
||||||
height: 1.55,
|
|
||||||
color: cs.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_PinInput(
|
_PinInput(
|
||||||
|
|||||||
@@ -27,23 +27,14 @@ class MallEntryController {
|
|||||||
if (config?.isVisible != true || !context.mounted) {
|
if (config?.isVisible != true || !context.mounted) {
|
||||||
return MallEntryOpenResult.unavailable;
|
return MallEntryOpenResult.unavailable;
|
||||||
}
|
}
|
||||||
final clickId = await _ref
|
config = await _ref
|
||||||
.read(mallTabConfigProvider.notifier)
|
.read(mallTabConfigProvider.notifier)
|
||||||
.recordClick();
|
.recordClick(request: request);
|
||||||
if (clickId == null || !context.mounted) {
|
if (!config.isVisible || !context.mounted) {
|
||||||
return MallEntryOpenResult.unavailable;
|
return MallEntryOpenResult.unavailable;
|
||||||
}
|
}
|
||||||
_ref
|
|
||||||
.read(mallClickAttributionProvider.notifier)
|
|
||||||
.state = MallClickAttribution(
|
|
||||||
adId: config!.adId,
|
|
||||||
trackingId: config.trackingId,
|
|
||||||
clickId: clickId,
|
|
||||||
);
|
|
||||||
final uri = config.attributedUri(
|
final uri = config.attributedUri(
|
||||||
clickId: clickId,
|
|
||||||
isDark: Theme.of(context).brightness == Brightness.dark,
|
isDark: Theme.of(context).brightness == Brightness.dark,
|
||||||
entry: request,
|
|
||||||
);
|
);
|
||||||
if (uri == null) return MallEntryOpenResult.unavailable;
|
if (uri == null) return MallEntryOpenResult.unavailable;
|
||||||
// The bottom mall entry is always resident. Server open-mode metadata is
|
// 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:dio/dio.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../../../common/network/dio_provider.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
|
import '../../../models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||||
|
|
||||||
/// 金值商城广告位的公共契约;商城是否展示完全由服务端配置决定。
|
/// 标识打开商城的宿主项目。
|
||||||
const mallSourceApp = 'jinzhi';
|
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 {
|
class MallEntryRequest {
|
||||||
const MallEntryRequest({
|
const MallEntryRequest({
|
||||||
required this.sourceEntry,
|
required this.sourceEntry,
|
||||||
@@ -45,11 +42,15 @@ class MallTabConfig {
|
|||||||
this.title = '商城',
|
this.title = '商城',
|
||||||
this.adId = '',
|
this.adId = '',
|
||||||
this.trackingId = '',
|
this.trackingId = '',
|
||||||
|
this.clickId = '',
|
||||||
this.openMode = 'in_app',
|
this.openMode = 'in_app',
|
||||||
this.entryPromoEnabled = false,
|
this.entryPromoEnabled = false,
|
||||||
this.entryBadgeText = '',
|
this.entryBadgeText = '',
|
||||||
this.entryTipTitle = '',
|
this.entryTipTitle = '',
|
||||||
this.entryTipBody = '',
|
this.entryTipBody = '',
|
||||||
|
this.quoteCtaEnabled = false,
|
||||||
|
this.entryRequest = MallEntryRequest.home,
|
||||||
|
this.allowedHosts = const <String>{},
|
||||||
});
|
});
|
||||||
|
|
||||||
const MallTabConfig.disabled() : this(enabled: false, url: '');
|
const MallTabConfig.disabled() : this(enabled: false, url: '');
|
||||||
@@ -59,11 +60,15 @@ class MallTabConfig {
|
|||||||
final String title;
|
final String title;
|
||||||
final String adId;
|
final String adId;
|
||||||
final String trackingId;
|
final String trackingId;
|
||||||
|
final String clickId;
|
||||||
final String openMode;
|
final String openMode;
|
||||||
final bool entryPromoEnabled;
|
final bool entryPromoEnabled;
|
||||||
final String entryBadgeText;
|
final String entryBadgeText;
|
||||||
final String entryTipTitle;
|
final String entryTipTitle;
|
||||||
final String entryTipBody;
|
final String entryTipBody;
|
||||||
|
final bool quoteCtaEnabled;
|
||||||
|
final MallEntryRequest entryRequest;
|
||||||
|
final Set<String> allowedHosts;
|
||||||
|
|
||||||
bool get isVisible => enabled && initialUri != null;
|
bool get isVisible => enabled && initialUri != null;
|
||||||
bool get opensInApp => openMode == 'in_app';
|
bool get opensInApp => openMode == 'in_app';
|
||||||
@@ -74,52 +79,80 @@ class MallTabConfig {
|
|||||||
entryPromoEnabled &&
|
entryPromoEnabled &&
|
||||||
(entryTipTitle.trim().isNotEmpty || entryTipBody.trim().isNotEmpty);
|
(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({
|
Uri? attributedUri({
|
||||||
required String clickId,
|
String? clickId,
|
||||||
bool isDark = false,
|
bool isDark = false,
|
||||||
MallEntryRequest entry = MallEntryRequest.home,
|
MallEntryRequest? entry,
|
||||||
}) {
|
}) {
|
||||||
final uri = Uri.tryParse(url.trim());
|
final uri = Uri.tryParse(url.trim());
|
||||||
if (!_isAllowed(uri)) return null;
|
if (!_isAllowed(uri)) return null;
|
||||||
|
final effectiveClickId = clickId ?? this.clickId;
|
||||||
|
final effectiveEntry = entry ?? entryRequest;
|
||||||
return uri!.replace(
|
return uri!.replace(
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
...uri.queryParameters,
|
...uri.queryParameters,
|
||||||
'from': uri.queryParameters['from']?.trim().isNotEmpty == true
|
'from': from,
|
||||||
? uri.queryParameters['from']!
|
'source_app': from,
|
||||||
: mallSourceApp,
|
'source_entry': effectiveEntry.sourceEntry,
|
||||||
'source_app': mallSourceApp,
|
|
||||||
'source_entry': entry.sourceEntry,
|
|
||||||
'placement': 'mall_tab',
|
'placement': 'mall_tab',
|
||||||
'hook_type': entry.hookType,
|
'hook_type': effectiveEntry.hookType,
|
||||||
if (entry.entryScene?.isNotEmpty == true)
|
'entry_scene': ?effectiveEntry.entryScene,
|
||||||
'entry_scene': entry.entryScene!,
|
'entry_metal': ?effectiveEntry.entryMetal,
|
||||||
if (entry.entryMetal?.isNotEmpty == true)
|
|
||||||
'entry_metal': entry.entryMetal!,
|
|
||||||
'target_app': 'mall',
|
'target_app': 'mall',
|
||||||
if (adId.isNotEmpty) 'ad_id': adId,
|
if (adId.isNotEmpty) 'ad_id': adId,
|
||||||
if (trackingId.isNotEmpty) 'tracking_id': trackingId,
|
if (trackingId.isNotEmpty) 'tracking_id': trackingId,
|
||||||
if (clickId.isNotEmpty) 'click_id': clickId,
|
if (effectiveClickId.isNotEmpty) 'click_id': effectiveClickId,
|
||||||
if (clickId.isNotEmpty)
|
if (effectiveClickId.isNotEmpty)
|
||||||
'ts': DateTime.now().millisecondsSinceEpoch.toString(),
|
'ts': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
'isNight': isDark ? '1' : '0',
|
'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) {
|
static bool _isAllowed(Uri? uri) {
|
||||||
if (uri == null || uri.host.isEmpty) return false;
|
if (uri == null || uri.host.isEmpty) return false;
|
||||||
if (uri.scheme == 'https') return true;
|
if (uri.scheme.toLowerCase() == 'https') return true;
|
||||||
return !kReleaseMode &&
|
return !kReleaseMode &&
|
||||||
uri.scheme == 'http' &&
|
uri.scheme.toLowerCase() == 'http' &&
|
||||||
const {'localhost', '127.0.0.1', '::1'}.contains(uri.host);
|
const {
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'::1',
|
||||||
|
}.contains(uri.host.toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
||||||
MallTabConfigController(this._dio) : super(const MallTabConfig.disabled()) {
|
MallTabConfigController(this._dio, {bool autoRefresh = true})
|
||||||
refresh();
|
: super(const MallTabConfig.disabled()) {
|
||||||
|
if (autoRefresh) refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
@@ -138,38 +171,51 @@ class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
|||||||
queryParameters: const {'placement': 'mall_tab'},
|
queryParameters: const {'placement': 'mall_tab'},
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
state = parseMallTabConfigResponse(response.data ?? const []);
|
state = parseMallTabConfigResponse(response.data ?? const <int>[]);
|
||||||
if (!state.isVisible) return;
|
if (!state.isVisible) return;
|
||||||
await _report('ad_impression');
|
await _report(state, 'ad_impression');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
state = const MallTabConfig.disabled();
|
state = const MallTabConfig.disabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> recordClick() async {
|
Future<MallTabConfig> recordClick({
|
||||||
if (!state.isVisible) return null;
|
MallEntryRequest request = MallEntryRequest.home,
|
||||||
final clickId = const Uuid().v4();
|
}) async {
|
||||||
|
if (!state.isVisible) return state;
|
||||||
|
final clicked = state.withClick(const Uuid().v4(), request: request);
|
||||||
|
state = clicked;
|
||||||
try {
|
try {
|
||||||
await _report('ad_click', clickId: clickId);
|
await _report(clicked, 'ad_click');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// 归因上报失败不能阻断商城入口。
|
// 归因上报失败不能阻断商城入口。
|
||||||
}
|
}
|
||||||
return clickId;
|
return clicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _report(String event, {String clickId = ''}) =>
|
Future<void> _report(MallTabConfig config, String event) async {
|
||||||
_dio.post<List<int>>(
|
final response = await _dio.post<List<int>>(
|
||||||
'/api/v1/ads/events',
|
'/api/v1/ads/events',
|
||||||
data: _ProtoWriter()
|
data: pb.ReportAdEventRequest(
|
||||||
.string(1, event)
|
eventName: event,
|
||||||
.string(2, 'mall_tab')
|
placement: 'mall_tab',
|
||||||
.string(3, state.adId)
|
adId: config.adId,
|
||||||
.string(4, state.trackingId)
|
trackingId: config.trackingId,
|
||||||
.string(5, clickId)
|
clickId: config.clickId,
|
||||||
.integer(6, DateTime.now().millisecondsSinceEpoch)
|
clientTsMs: Int64(DateTime.now().millisecondsSinceEpoch),
|
||||||
.toBytes(),
|
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(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
|
pb.ReportAdEventReply.fromBuffer(response.data ?? const <int>[]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final mallTabConfigProvider =
|
final mallTabConfigProvider =
|
||||||
@@ -177,52 +223,21 @@ final mallTabConfigProvider =
|
|||||||
(ref) => MallTabConfigController(ref.watch(dioProvider)),
|
(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({
|
Map<String, dynamic> buildEnterWebMallBridgeArguments({
|
||||||
required Uri currentUri,
|
required MallTabConfig config,
|
||||||
required bool logged,
|
required bool logged,
|
||||||
required bool isDark,
|
Uri? currentUri,
|
||||||
|
bool isDark = false,
|
||||||
String? authToken,
|
String? authToken,
|
||||||
String? scene,
|
String? scene,
|
||||||
String? sourceApp,
|
String? sourceApp,
|
||||||
MallClickAttribution? attribution,
|
|
||||||
}) {
|
}) {
|
||||||
final query = currentUri.queryParameters;
|
|
||||||
final resolvedSourceApp = sourceApp?.trim().isNotEmpty == true
|
|
||||||
? sourceApp!.trim()
|
|
||||||
: mallSourceApp;
|
|
||||||
final arguments = <String, dynamic>{
|
final arguments = <String, dynamic>{
|
||||||
...query,
|
...?currentUri?.queryParameters,
|
||||||
'from': resolvedSourceApp,
|
...?config.attributedUri(isDark: isDark)?.queryParameters,
|
||||||
'source_app': resolvedSourceApp,
|
'from': config.from,
|
||||||
'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,
|
'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() ?? '';
|
final token = authToken?.trim() ?? '';
|
||||||
if (token.isNotEmpty) {
|
if (token.isNotEmpty) {
|
||||||
arguments['auth_token'] = token;
|
arguments['auth_token'] = token;
|
||||||
@@ -230,119 +245,52 @@ Map<String, dynamic> buildEnterWebMallBridgeArguments({
|
|||||||
}
|
}
|
||||||
final resolvedScene = scene?.trim() ?? '';
|
final resolvedScene = scene?.trim() ?? '';
|
||||||
if (resolvedScene.isNotEmpty) arguments['scene'] = resolvedScene;
|
if (resolvedScene.isNotEmpty) arguments['scene'] = resolvedScene;
|
||||||
|
final resolvedSourceApp = sourceApp?.trim() ?? '';
|
||||||
|
if (resolvedSourceApp.isNotEmpty) {
|
||||||
|
arguments['from'] = resolvedSourceApp;
|
||||||
|
arguments['source_app'] = resolvedSourceApp;
|
||||||
|
}
|
||||||
return arguments;
|
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) =>
|
String _nonEmpty(String value, String fallback) =>
|
||||||
value.trim().isEmpty ? fallback : value.trim();
|
value.trim().isEmpty ? fallback : value.trim();
|
||||||
|
|
||||||
String? _validUrl(String raw) {
|
String _openMode(pb.AdOpenMode value) => switch (value) {
|
||||||
final uri = Uri.tryParse(raw.trim());
|
pb.AdOpenMode.AD_OPEN_MODE_NATIVE_APP => 'native_app',
|
||||||
return MallTabConfig._isAllowed(uri) ? uri.toString() : null;
|
pb.AdOpenMode.AD_OPEN_MODE_IN_APP => 'in_app',
|
||||||
}
|
|
||||||
|
|
||||||
String _openMode(int value) => switch (value) {
|
|
||||||
1 => 'native_app',
|
|
||||||
2 => 'in_app',
|
|
||||||
_ => 'external',
|
_ => 'external',
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parses the protobuf reply returned by `/api/v1/ads?placement=mall_tab`.
|
String? _firstAllowedUrl(String trackedUrl, String h5Url) {
|
||||||
MallTabConfig parseMallTabConfigResponse(List<int> bytes) {
|
for (final raw in <String>[trackedUrl, h5Url]) {
|
||||||
final response = _ProtoMessage(bytes);
|
final uri = Uri.tryParse(raw.trim());
|
||||||
final reply = response.message(2);
|
if (MallTabConfig._isAllowed(uri)) return uri.toString();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,8 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
final cs = ShadTheme.of(context).colorScheme;
|
final cs = ShadTheme.of(context).colorScheme;
|
||||||
final loggedIn = auth.valueOrNull is LoggedInAuthState;
|
final loggedIn = auth.valueOrNull is LoggedInAuthState;
|
||||||
|
final authLoading = auth.isLoading;
|
||||||
|
final authFailed = auth.hasError;
|
||||||
final currentUser = auth.valueOrNull is LoggedInAuthState
|
final currentUser = auth.valueOrNull is LoggedInAuthState
|
||||||
? (auth.valueOrNull as LoggedInAuthState).user
|
? (auth.valueOrNull as LoggedInAuthState).user
|
||||||
: null;
|
: null;
|
||||||
@@ -100,8 +102,22 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
FilledButton.tonal(
|
FilledButton.tonal(
|
||||||
onPressed: () => LoginLauncher.start(context, ref),
|
onPressed: authLoading
|
||||||
child: Text(loggedIn ? '切换账号' : '去登录'),
|
? null
|
||||||
|
: authFailed
|
||||||
|
? () => ref
|
||||||
|
.read(authProvider.notifier)
|
||||||
|
.retryBootstrap()
|
||||||
|
: () => LoginLauncher.start(context, ref),
|
||||||
|
child: Text(
|
||||||
|
authLoading
|
||||||
|
? '正在恢复账号…'
|
||||||
|
: authFailed
|
||||||
|
? '账号恢复失败,点击重试'
|
||||||
|
: loggedIn
|
||||||
|
? '切换账号'
|
||||||
|
: '去登录',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../../../common/config/app_h5_urls.dart';
|
import '../../../common/config/app_h5_urls.dart';
|
||||||
import '../../../common/push/notification_permission.dart';
|
import '../../../common/push/notification_permission.dart';
|
||||||
@@ -32,6 +33,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final cs = ShadTheme.of(context).colorScheme;
|
final cs = ShadTheme.of(context).colorScheme;
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final themeMode = ref.watch(themeModeControllerProvider);
|
final themeMode = ref.watch(themeModeControllerProvider);
|
||||||
final profileSettings = ref.watch(profileSettingsControllerProvider);
|
final profileSettings = ref.watch(profileSettingsControllerProvider);
|
||||||
final authAsync = ref.watch(authProvider);
|
final authAsync = ref.watch(authProvider);
|
||||||
@@ -198,16 +200,28 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
_NavRow(
|
_NavRow(
|
||||||
icon: FlutterRemix.file_list_3_line,
|
icon: FlutterRemix.file_list_3_line,
|
||||||
label: '用户协议',
|
label: '用户协议',
|
||||||
onTap: () => context.push(
|
onTap: () => launchUrlString(
|
||||||
'${AppRoutes.web}?url=${Uri.encodeComponent(AppH5UrlsHelper.withCacheBuster(AppH5Urls.userProtocolUrl))}',
|
AppH5UrlsHelper.withCacheBuster(
|
||||||
|
AppH5UrlsHelper.buildUrlWithNight(
|
||||||
|
AppH5Urls.userProtocolUrl,
|
||||||
|
isDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mode: LaunchMode.inAppBrowserView,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(height: 1, thickness: 1, color: cs.border),
|
Divider(height: 1, thickness: 1, color: cs.border),
|
||||||
_NavRow(
|
_NavRow(
|
||||||
icon: FlutterRemix.shield_check_line,
|
icon: FlutterRemix.shield_check_line,
|
||||||
label: '隐私政策',
|
label: '隐私政策',
|
||||||
onTap: () => context.push(
|
onTap: () => launchUrlString(
|
||||||
'${AppRoutes.web}?url=${Uri.encodeComponent(AppH5UrlsHelper.withCacheBuster(AppH5Urls.privacyUrl))}',
|
AppH5UrlsHelper.withCacheBuster(
|
||||||
|
AppH5UrlsHelper.buildUrlWithNight(
|
||||||
|
AppH5Urls.privacyUrl,
|
||||||
|
isDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mode: LaunchMode.inAppBrowserView,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(height: 1, thickness: 1, color: cs.border),
|
Divider(height: 1, thickness: 1, color: cs.border),
|
||||||
|
|||||||
@@ -571,13 +571,13 @@ class _MallWebHostState extends ConsumerState<MallWebHost>
|
|||||||
final mallAuth = logged ? await _issueMallAuthToken() : null;
|
final mallAuth = logged ? await _issueMallAuthToken() : null;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final arguments = buildEnterWebMallBridgeArguments(
|
final arguments = buildEnterWebMallBridgeArguments(
|
||||||
|
config: ref.read(mallTabConfigProvider),
|
||||||
currentUri: uri!,
|
currentUri: uri!,
|
||||||
logged: logged,
|
logged: logged,
|
||||||
isDark: Theme.of(context).brightness == Brightness.dark,
|
isDark: Theme.of(context).brightness == Brightness.dark,
|
||||||
authToken: mallAuth?.authToken,
|
authToken: mallAuth?.authToken,
|
||||||
scene: mallAuth?.scene,
|
scene: mallAuth?.scene,
|
||||||
sourceApp: mallAuth?.sourceApp,
|
sourceApp: mallAuth?.sourceApp,
|
||||||
attribution: ref.read(mallClickAttributionProvider),
|
|
||||||
);
|
);
|
||||||
_callH5Handler(JsEvent('enterWebMall', [arguments]));
|
_callH5Handler(JsEvent('enterWebMall', [arguments]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
|
import '../application/web_navigation_policy.dart';
|
||||||
|
|
||||||
/// Shared HTTPS WebView host. Mall-specific residency and attribution are
|
/// Shared HTTPS WebView host. Mall-specific residency and attribution are
|
||||||
/// layered above this page so ordinary H5 links never receive mall state.
|
/// layered above this page so ordinary H5 links never receive mall state.
|
||||||
class AppWebPage extends StatefulWidget {
|
class AppWebPage extends StatefulWidget {
|
||||||
@@ -10,12 +13,10 @@ class AppWebPage extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.initialUri,
|
required this.initialUri,
|
||||||
this.hideTitle = false,
|
this.hideTitle = false,
|
||||||
this.allowedHosts = const <String>{},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri initialUri;
|
final Uri initialUri;
|
||||||
final bool hideTitle;
|
final bool hideTitle;
|
||||||
final Set<String> allowedHosts;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppWebPage> createState() => _AppWebPageState();
|
State<AppWebPage> createState() => _AppWebPageState();
|
||||||
@@ -60,22 +61,18 @@ class _AppWebPageState extends State<AppWebPage> {
|
|||||||
|
|
||||||
NavigationDecision _onNavigationRequest(NavigationRequest request) {
|
NavigationDecision _onNavigationRequest(NavigationRequest request) {
|
||||||
final uri = Uri.tryParse(request.url);
|
final uri = Uri.tryParse(request.url);
|
||||||
if (uri == null || !_isHttp(uri)) return NavigationDecision.prevent;
|
if (uri != null && shouldOpenWebUrlExternally(request.url)) {
|
||||||
if (_allowsInApp(uri)) return NavigationDecision.navigate;
|
|
||||||
launchUrl(uri, mode: LaunchMode.externalApplication);
|
launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
return NavigationDecision.prevent;
|
return NavigationDecision.prevent;
|
||||||
}
|
}
|
||||||
|
if (uri == null || !_isAllowedWebUri(uri)) {
|
||||||
bool _allowsInApp(Uri uri) {
|
return NavigationDecision.prevent;
|
||||||
final host = uri.host.toLowerCase();
|
}
|
||||||
if (host == widget.initialUri.host.toLowerCase()) return true;
|
// Align with jinzhi-flutter-app: a normal HTTPS navigation stays in this
|
||||||
return widget.allowedHosts.any(
|
// WebView even when its host changes. H5 opts into an external handoff
|
||||||
(allowed) => host == allowed || host.endsWith('.$allowed'),
|
// through `out`, which is handled above.
|
||||||
);
|
return NavigationDecision.navigate;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isHttp(Uri uri) =>
|
|
||||||
uri.hasAuthority && (uri.scheme == 'https' || uri.scheme == 'http');
|
|
||||||
|
|
||||||
Future<bool> _onPop() async {
|
Future<bool> _onPop() async {
|
||||||
if (await _controller.canGoBack()) {
|
if (await _controller.canGoBack()) {
|
||||||
@@ -118,6 +115,16 @@ class _AppWebPageState extends State<AppWebPage> {
|
|||||||
|
|
||||||
Uri? parseSafeWebUri(String? raw) {
|
Uri? parseSafeWebUri(String? raw) {
|
||||||
final uri = Uri.tryParse(raw?.trim() ?? '');
|
final uri = Uri.tryParse(raw?.trim() ?? '');
|
||||||
if (uri == null || !uri.hasAuthority) return null;
|
return uri != null && _isAllowedWebUri(uri) ? uri : null;
|
||||||
return uri.scheme == 'https' || uri.scheme == 'http' ? uri : null;
|
}
|
||||||
|
|
||||||
|
bool _isAllowedWebUri(Uri uri) {
|
||||||
|
if (!uri.hasAuthority || uri.host.isEmpty) return false;
|
||||||
|
if (uri.scheme.toLowerCase() == 'https') return true;
|
||||||
|
if (kReleaseMode || uri.scheme.toLowerCase() != 'http') return false;
|
||||||
|
return const {
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'::1',
|
||||||
|
}.contains(uri.host.toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: errors/errors.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
class Status extends $pb.GeneratedMessage {
|
||||||
|
factory Status({
|
||||||
|
$core.int? code,
|
||||||
|
$core.String? reason,
|
||||||
|
$core.String? message,
|
||||||
|
$core.Map<$core.String, $core.String>? metadata,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (code != null) {
|
||||||
|
$result.code = code;
|
||||||
|
}
|
||||||
|
if (reason != null) {
|
||||||
|
$result.reason = reason;
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
$result.message = message;
|
||||||
|
}
|
||||||
|
if (metadata != null) {
|
||||||
|
$result.metadata.addAll(metadata);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
Status._() : super();
|
||||||
|
factory Status.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory Status.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Status', package: const $pb.PackageName(_omitMessageNames ? '' : 'errors'), createEmptyInstance: create)
|
||||||
|
..a<$core.int>(1, _omitFieldNames ? '' : 'code', $pb.PbFieldType.O3)
|
||||||
|
..aOS(2, _omitFieldNames ? '' : 'reason')
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'message')
|
||||||
|
..m<$core.String, $core.String>(4, _omitFieldNames ? '' : 'metadata', entryClassName: 'Status.MetadataEntry', keyFieldType: $pb.PbFieldType.OS, valueFieldType: $pb.PbFieldType.OS, packageName: const $pb.PackageName('errors'))
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Status clone() => Status()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Status copyWith(void Function(Status) updates) => super.copyWith((message) => updates(message as Status)) as Status;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Status create() => Status._();
|
||||||
|
Status createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<Status> createRepeated() => $pb.PbList<Status>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Status getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Status>(create);
|
||||||
|
static Status? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.int get code => $_getIZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set code($core.int v) { $_setSignedInt32(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasCode() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearCode() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.String get reason => $_getSZ(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set reason($core.String v) { $_setString(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasReason() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearReason() => clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get message => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set message($core.String v) { $_setString(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasMessage() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearMessage() => clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.Map<$core.String, $core.String> get metadata => $_getMap(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Errors {
|
||||||
|
static final defaultCode = $pb.Extension<$core.int>(_omitMessageNames ? '' : 'google.protobuf.EnumOptions', _omitFieldNames ? '' : 'defaultCode', 1108, $pb.PbFieldType.O3);
|
||||||
|
static final code = $pb.Extension<$core.int>(_omitMessageNames ? '' : 'google.protobuf.EnumValueOptions', _omitFieldNames ? '' : 'code', 1109, $pb.PbFieldType.O3);
|
||||||
|
static void registerAllExtensions($pb.ExtensionRegistry registry) {
|
||||||
|
registry.add(defaultCode);
|
||||||
|
registry.add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||||
|
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: errors/errors.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: errors/errors.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:convert' as $convert;
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
import 'dart:typed_data' as $typed_data;
|
||||||
|
|
||||||
|
@$core.Deprecated('Use statusDescriptor instead')
|
||||||
|
const Status$json = {
|
||||||
|
'1': 'Status',
|
||||||
|
'2': [
|
||||||
|
{'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'},
|
||||||
|
{'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'},
|
||||||
|
{'1': 'message', '3': 3, '4': 1, '5': 9, '10': 'message'},
|
||||||
|
{'1': 'metadata', '3': 4, '4': 3, '5': 11, '6': '.errors.Status.MetadataEntry', '10': 'metadata'},
|
||||||
|
],
|
||||||
|
'3': [Status_MetadataEntry$json],
|
||||||
|
};
|
||||||
|
|
||||||
|
@$core.Deprecated('Use statusDescriptor instead')
|
||||||
|
const Status_MetadataEntry$json = {
|
||||||
|
'1': 'MetadataEntry',
|
||||||
|
'2': [
|
||||||
|
{'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
|
||||||
|
{'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'},
|
||||||
|
],
|
||||||
|
'7': {'7': true},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `Status`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List statusDescriptor = $convert.base64Decode(
|
||||||
|
'CgZTdGF0dXMSEgoEY29kZRgBIAEoBVIEY29kZRIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhIYCg'
|
||||||
|
'dtZXNzYWdlGAMgASgJUgdtZXNzYWdlEjgKCG1ldGFkYXRhGAQgAygLMhwuZXJyb3JzLlN0YXR1'
|
||||||
|
'cy5NZXRhZGF0YUVudHJ5UghtZXRhZGF0YRo7Cg1NZXRhZGF0YUVudHJ5EhAKA2tleRgBIAEoCV'
|
||||||
|
'IDa2V5EhQKBXZhbHVlGAIgASgJUgV2YWx1ZToCOAE=');
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: errors/errors.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
export 'errors.pb.dart';
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: google/protobuf/empty.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
/// A generic empty message that you can re-use to avoid defining duplicated
|
||||||
|
/// empty messages in your APIs. A typical example is to use it as the request
|
||||||
|
/// or the response type of an API method. For instance:
|
||||||
|
///
|
||||||
|
/// service Foo {
|
||||||
|
/// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// The JSON representation for `Empty` is empty JSON object `{}`.
|
||||||
|
class Empty extends $pb.GeneratedMessage {
|
||||||
|
factory Empty() => create();
|
||||||
|
Empty._() : super();
|
||||||
|
factory Empty.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory Empty.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Empty', package: const $pb.PackageName(_omitMessageNames ? '' : 'google.protobuf'), createEmptyInstance: create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Empty clone() => Empty()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
Empty copyWith(void Function(Empty) updates) => super.copyWith((message) => updates(message as Empty)) as Empty;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Empty create() => Empty._();
|
||||||
|
Empty createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static Empty getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create);
|
||||||
|
static Empty? _defaultInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,868 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: jinzhi/v1/ads.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:async' as $async;
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
import 'ads.pbenum.dart';
|
||||||
|
|
||||||
|
export 'ads.pbenum.dart';
|
||||||
|
|
||||||
|
class GetAdRequest extends $pb.GeneratedMessage {
|
||||||
|
factory GetAdRequest({
|
||||||
|
$core.String? placement,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (placement != null) {
|
||||||
|
$result.placement = placement;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
GetAdRequest._() : super();
|
||||||
|
factory GetAdRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory GetAdRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetAdRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'placement')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
GetAdRequest clone() => GetAdRequest()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
GetAdRequest copyWith(void Function(GetAdRequest) updates) => super.copyWith((message) => updates(message as GetAdRequest)) as GetAdRequest;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static GetAdRequest create() => GetAdRequest._();
|
||||||
|
GetAdRequest createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<GetAdRequest> createRepeated() => $pb.PbList<GetAdRequest>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static GetAdRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetAdRequest>(create);
|
||||||
|
static GetAdRequest? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get placement => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set placement($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasPlacement() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearPlacement() => clearField(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetAdReply extends $pb.GeneratedMessage {
|
||||||
|
factory GetAdReply({
|
||||||
|
$core.bool? enabled,
|
||||||
|
AdCreative? ad,
|
||||||
|
$core.int? dailyCap,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (enabled != null) {
|
||||||
|
$result.enabled = enabled;
|
||||||
|
}
|
||||||
|
if (ad != null) {
|
||||||
|
$result.ad = ad;
|
||||||
|
}
|
||||||
|
if (dailyCap != null) {
|
||||||
|
$result.dailyCap = dailyCap;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
GetAdReply._() : super();
|
||||||
|
factory GetAdReply.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory GetAdReply.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetAdReply', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOB(1, _omitFieldNames ? '' : 'enabled')
|
||||||
|
..aOM<AdCreative>(2, _omitFieldNames ? '' : 'ad', subBuilder: AdCreative.create)
|
||||||
|
..a<$core.int>(3, _omitFieldNames ? '' : 'dailyCap', $pb.PbFieldType.O3)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
GetAdReply clone() => GetAdReply()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
GetAdReply copyWith(void Function(GetAdReply) updates) => super.copyWith((message) => updates(message as GetAdReply)) as GetAdReply;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static GetAdReply create() => GetAdReply._();
|
||||||
|
GetAdReply createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<GetAdReply> createRepeated() => $pb.PbList<GetAdReply>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static GetAdReply getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetAdReply>(create);
|
||||||
|
static GetAdReply? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool get enabled => $_getBF(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set enabled($core.bool v) { $_setBool(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasEnabled() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearEnabled() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
AdCreative get ad => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set ad(AdCreative v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasAd() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearAd() => clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
AdCreative ensureAd() => $_ensure(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.int get dailyCap => $_getIZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set dailyCap($core.int v) { $_setSignedInt32(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasDailyCap() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearDailyCap() => clearField(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListAdsRequest extends $pb.GeneratedMessage {
|
||||||
|
factory ListAdsRequest({
|
||||||
|
$core.String? placement,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (placement != null) {
|
||||||
|
$result.placement = placement;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
ListAdsRequest._() : super();
|
||||||
|
factory ListAdsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory ListAdsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ListAdsRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'placement')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ListAdsRequest clone() => ListAdsRequest()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ListAdsRequest copyWith(void Function(ListAdsRequest) updates) => super.copyWith((message) => updates(message as ListAdsRequest)) as ListAdsRequest;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ListAdsRequest create() => ListAdsRequest._();
|
||||||
|
ListAdsRequest createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<ListAdsRequest> createRepeated() => $pb.PbList<ListAdsRequest>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ListAdsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListAdsRequest>(create);
|
||||||
|
static ListAdsRequest? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get placement => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set placement($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasPlacement() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearPlacement() => clearField(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListAdsReply extends $pb.GeneratedMessage {
|
||||||
|
factory ListAdsReply({
|
||||||
|
$core.bool? enabled,
|
||||||
|
$core.Iterable<AdCreative>? ads,
|
||||||
|
$core.String? trackingId,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (enabled != null) {
|
||||||
|
$result.enabled = enabled;
|
||||||
|
}
|
||||||
|
if (ads != null) {
|
||||||
|
$result.ads.addAll(ads);
|
||||||
|
}
|
||||||
|
if (trackingId != null) {
|
||||||
|
$result.trackingId = trackingId;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
ListAdsReply._() : super();
|
||||||
|
factory ListAdsReply.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory ListAdsReply.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ListAdsReply', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOB(1, _omitFieldNames ? '' : 'enabled')
|
||||||
|
..pc<AdCreative>(2, _omitFieldNames ? '' : 'ads', $pb.PbFieldType.PM, subBuilder: AdCreative.create)
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'trackingId')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ListAdsReply clone() => ListAdsReply()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ListAdsReply copyWith(void Function(ListAdsReply) updates) => super.copyWith((message) => updates(message as ListAdsReply)) as ListAdsReply;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ListAdsReply create() => ListAdsReply._();
|
||||||
|
ListAdsReply createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<ListAdsReply> createRepeated() => $pb.PbList<ListAdsReply>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ListAdsReply getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ListAdsReply>(create);
|
||||||
|
static ListAdsReply? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool get enabled => $_getBF(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set enabled($core.bool v) { $_setBool(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasEnabled() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearEnabled() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.List<AdCreative> get ads => $_getList(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get trackingId => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set trackingId($core.String v) { $_setString(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasTrackingId() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearTrackingId() => clearField(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdCreative extends $pb.GeneratedMessage {
|
||||||
|
factory AdCreative({
|
||||||
|
$core.String? adId,
|
||||||
|
$core.String? placement,
|
||||||
|
$core.String? targetApp,
|
||||||
|
$core.String? title,
|
||||||
|
$core.String? subtitle,
|
||||||
|
$core.String? imageUrl,
|
||||||
|
MediaType? mediaType,
|
||||||
|
$core.String? ctaText,
|
||||||
|
$core.String? h5Url,
|
||||||
|
$core.String? trackingId,
|
||||||
|
$core.int? countdownSeconds,
|
||||||
|
$core.int? priority,
|
||||||
|
$fixnum.Int64? endAtMs,
|
||||||
|
$core.String? trackedUrl,
|
||||||
|
AdOpenMode? openMode,
|
||||||
|
$core.bool? entryPromoEnabled,
|
||||||
|
$core.String? entryBadgeText,
|
||||||
|
$core.String? entryTipTitle,
|
||||||
|
$core.String? entryTipBody,
|
||||||
|
MallPromotion? mallPromotion,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (adId != null) {
|
||||||
|
$result.adId = adId;
|
||||||
|
}
|
||||||
|
if (placement != null) {
|
||||||
|
$result.placement = placement;
|
||||||
|
}
|
||||||
|
if (targetApp != null) {
|
||||||
|
$result.targetApp = targetApp;
|
||||||
|
}
|
||||||
|
if (title != null) {
|
||||||
|
$result.title = title;
|
||||||
|
}
|
||||||
|
if (subtitle != null) {
|
||||||
|
$result.subtitle = subtitle;
|
||||||
|
}
|
||||||
|
if (imageUrl != null) {
|
||||||
|
$result.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
if (mediaType != null) {
|
||||||
|
$result.mediaType = mediaType;
|
||||||
|
}
|
||||||
|
if (ctaText != null) {
|
||||||
|
$result.ctaText = ctaText;
|
||||||
|
}
|
||||||
|
if (h5Url != null) {
|
||||||
|
$result.h5Url = h5Url;
|
||||||
|
}
|
||||||
|
if (trackingId != null) {
|
||||||
|
$result.trackingId = trackingId;
|
||||||
|
}
|
||||||
|
if (countdownSeconds != null) {
|
||||||
|
$result.countdownSeconds = countdownSeconds;
|
||||||
|
}
|
||||||
|
if (priority != null) {
|
||||||
|
$result.priority = priority;
|
||||||
|
}
|
||||||
|
if (endAtMs != null) {
|
||||||
|
$result.endAtMs = endAtMs;
|
||||||
|
}
|
||||||
|
if (trackedUrl != null) {
|
||||||
|
$result.trackedUrl = trackedUrl;
|
||||||
|
}
|
||||||
|
if (openMode != null) {
|
||||||
|
$result.openMode = openMode;
|
||||||
|
}
|
||||||
|
if (entryPromoEnabled != null) {
|
||||||
|
$result.entryPromoEnabled = entryPromoEnabled;
|
||||||
|
}
|
||||||
|
if (entryBadgeText != null) {
|
||||||
|
$result.entryBadgeText = entryBadgeText;
|
||||||
|
}
|
||||||
|
if (entryTipTitle != null) {
|
||||||
|
$result.entryTipTitle = entryTipTitle;
|
||||||
|
}
|
||||||
|
if (entryTipBody != null) {
|
||||||
|
$result.entryTipBody = entryTipBody;
|
||||||
|
}
|
||||||
|
if (mallPromotion != null) {
|
||||||
|
$result.mallPromotion = mallPromotion;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
AdCreative._() : super();
|
||||||
|
factory AdCreative.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory AdCreative.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AdCreative', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'adId')
|
||||||
|
..aOS(2, _omitFieldNames ? '' : 'placement')
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'targetApp')
|
||||||
|
..aOS(4, _omitFieldNames ? '' : 'title')
|
||||||
|
..aOS(5, _omitFieldNames ? '' : 'subtitle')
|
||||||
|
..aOS(6, _omitFieldNames ? '' : 'imageUrl')
|
||||||
|
..e<MediaType>(7, _omitFieldNames ? '' : 'mediaType', $pb.PbFieldType.OE, defaultOrMaker: MediaType.MEDIA_TYPE_UNSPECIFIED, valueOf: MediaType.valueOf, enumValues: MediaType.values)
|
||||||
|
..aOS(8, _omitFieldNames ? '' : 'ctaText')
|
||||||
|
..aOS(9, _omitFieldNames ? '' : 'h5Url')
|
||||||
|
..aOS(10, _omitFieldNames ? '' : 'trackingId')
|
||||||
|
..a<$core.int>(11, _omitFieldNames ? '' : 'countdownSeconds', $pb.PbFieldType.O3)
|
||||||
|
..a<$core.int>(12, _omitFieldNames ? '' : 'priority', $pb.PbFieldType.O3)
|
||||||
|
..aInt64(13, _omitFieldNames ? '' : 'endAtMs')
|
||||||
|
..aOS(14, _omitFieldNames ? '' : 'trackedUrl')
|
||||||
|
..e<AdOpenMode>(15, _omitFieldNames ? '' : 'openMode', $pb.PbFieldType.OE, defaultOrMaker: AdOpenMode.AD_OPEN_MODE_UNSPECIFIED, valueOf: AdOpenMode.valueOf, enumValues: AdOpenMode.values)
|
||||||
|
..aOB(16, _omitFieldNames ? '' : 'entryPromoEnabled')
|
||||||
|
..aOS(17, _omitFieldNames ? '' : 'entryBadgeText')
|
||||||
|
..aOS(18, _omitFieldNames ? '' : 'entryTipTitle')
|
||||||
|
..aOS(19, _omitFieldNames ? '' : 'entryTipBody')
|
||||||
|
..aOM<MallPromotion>(20, _omitFieldNames ? '' : 'mallPromotion', subBuilder: MallPromotion.create)
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
AdCreative clone() => AdCreative()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
AdCreative copyWith(void Function(AdCreative) updates) => super.copyWith((message) => updates(message as AdCreative)) as AdCreative;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static AdCreative create() => AdCreative._();
|
||||||
|
AdCreative createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<AdCreative> createRepeated() => $pb.PbList<AdCreative>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static AdCreative getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AdCreative>(create);
|
||||||
|
static AdCreative? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get adId => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set adId($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasAdId() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearAdId() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.String get placement => $_getSZ(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set placement($core.String v) { $_setString(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasPlacement() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearPlacement() => clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get targetApp => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set targetApp($core.String v) { $_setString(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasTargetApp() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearTargetApp() => clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.String get title => $_getSZ(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set title($core.String v) { $_setString(3, v); }
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasTitle() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearTitle() => clearField(4);
|
||||||
|
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.String get subtitle => $_getSZ(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
set subtitle($core.String v) { $_setString(4, v); }
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool hasSubtitle() => $_has(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
void clearSubtitle() => clearField(5);
|
||||||
|
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.String get imageUrl => $_getSZ(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
set imageUrl($core.String v) { $_setString(5, v); }
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.bool hasImageUrl() => $_has(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
void clearImageUrl() => clearField(6);
|
||||||
|
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
MediaType get mediaType => $_getN(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
set mediaType(MediaType v) { setField(7, v); }
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.bool hasMediaType() => $_has(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
void clearMediaType() => clearField(7);
|
||||||
|
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.String get ctaText => $_getSZ(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
set ctaText($core.String v) { $_setString(7, v); }
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.bool hasCtaText() => $_has(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
void clearCtaText() => clearField(8);
|
||||||
|
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
$core.String get h5Url => $_getSZ(8);
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
set h5Url($core.String v) { $_setString(8, v); }
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
$core.bool hasH5Url() => $_has(8);
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
void clearH5Url() => clearField(9);
|
||||||
|
|
||||||
|
@$pb.TagNumber(10)
|
||||||
|
$core.String get trackingId => $_getSZ(9);
|
||||||
|
@$pb.TagNumber(10)
|
||||||
|
set trackingId($core.String v) { $_setString(9, v); }
|
||||||
|
@$pb.TagNumber(10)
|
||||||
|
$core.bool hasTrackingId() => $_has(9);
|
||||||
|
@$pb.TagNumber(10)
|
||||||
|
void clearTrackingId() => clearField(10);
|
||||||
|
|
||||||
|
@$pb.TagNumber(11)
|
||||||
|
$core.int get countdownSeconds => $_getIZ(10);
|
||||||
|
@$pb.TagNumber(11)
|
||||||
|
set countdownSeconds($core.int v) { $_setSignedInt32(10, v); }
|
||||||
|
@$pb.TagNumber(11)
|
||||||
|
$core.bool hasCountdownSeconds() => $_has(10);
|
||||||
|
@$pb.TagNumber(11)
|
||||||
|
void clearCountdownSeconds() => clearField(11);
|
||||||
|
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
$core.int get priority => $_getIZ(11);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
set priority($core.int v) { $_setSignedInt32(11, v); }
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
$core.bool hasPriority() => $_has(11);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
void clearPriority() => clearField(12);
|
||||||
|
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
$fixnum.Int64 get endAtMs => $_getI64(12);
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
set endAtMs($fixnum.Int64 v) { $_setInt64(12, v); }
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
$core.bool hasEndAtMs() => $_has(12);
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
void clearEndAtMs() => clearField(13);
|
||||||
|
|
||||||
|
@$pb.TagNumber(14)
|
||||||
|
$core.String get trackedUrl => $_getSZ(13);
|
||||||
|
@$pb.TagNumber(14)
|
||||||
|
set trackedUrl($core.String v) { $_setString(13, v); }
|
||||||
|
@$pb.TagNumber(14)
|
||||||
|
$core.bool hasTrackedUrl() => $_has(13);
|
||||||
|
@$pb.TagNumber(14)
|
||||||
|
void clearTrackedUrl() => clearField(14);
|
||||||
|
|
||||||
|
@$pb.TagNumber(15)
|
||||||
|
AdOpenMode get openMode => $_getN(14);
|
||||||
|
@$pb.TagNumber(15)
|
||||||
|
set openMode(AdOpenMode v) { setField(15, v); }
|
||||||
|
@$pb.TagNumber(15)
|
||||||
|
$core.bool hasOpenMode() => $_has(14);
|
||||||
|
@$pb.TagNumber(15)
|
||||||
|
void clearOpenMode() => clearField(15);
|
||||||
|
|
||||||
|
@$pb.TagNumber(16)
|
||||||
|
$core.bool get entryPromoEnabled => $_getBF(15);
|
||||||
|
@$pb.TagNumber(16)
|
||||||
|
set entryPromoEnabled($core.bool v) { $_setBool(15, v); }
|
||||||
|
@$pb.TagNumber(16)
|
||||||
|
$core.bool hasEntryPromoEnabled() => $_has(15);
|
||||||
|
@$pb.TagNumber(16)
|
||||||
|
void clearEntryPromoEnabled() => clearField(16);
|
||||||
|
|
||||||
|
@$pb.TagNumber(17)
|
||||||
|
$core.String get entryBadgeText => $_getSZ(16);
|
||||||
|
@$pb.TagNumber(17)
|
||||||
|
set entryBadgeText($core.String v) { $_setString(16, v); }
|
||||||
|
@$pb.TagNumber(17)
|
||||||
|
$core.bool hasEntryBadgeText() => $_has(16);
|
||||||
|
@$pb.TagNumber(17)
|
||||||
|
void clearEntryBadgeText() => clearField(17);
|
||||||
|
|
||||||
|
@$pb.TagNumber(18)
|
||||||
|
$core.String get entryTipTitle => $_getSZ(17);
|
||||||
|
@$pb.TagNumber(18)
|
||||||
|
set entryTipTitle($core.String v) { $_setString(17, v); }
|
||||||
|
@$pb.TagNumber(18)
|
||||||
|
$core.bool hasEntryTipTitle() => $_has(17);
|
||||||
|
@$pb.TagNumber(18)
|
||||||
|
void clearEntryTipTitle() => clearField(18);
|
||||||
|
|
||||||
|
@$pb.TagNumber(19)
|
||||||
|
$core.String get entryTipBody => $_getSZ(18);
|
||||||
|
@$pb.TagNumber(19)
|
||||||
|
set entryTipBody($core.String v) { $_setString(18, v); }
|
||||||
|
@$pb.TagNumber(19)
|
||||||
|
$core.bool hasEntryTipBody() => $_has(18);
|
||||||
|
@$pb.TagNumber(19)
|
||||||
|
void clearEntryTipBody() => clearField(19);
|
||||||
|
|
||||||
|
@$pb.TagNumber(20)
|
||||||
|
MallPromotion get mallPromotion => $_getN(19);
|
||||||
|
@$pb.TagNumber(20)
|
||||||
|
set mallPromotion(MallPromotion v) { setField(20, v); }
|
||||||
|
@$pb.TagNumber(20)
|
||||||
|
$core.bool hasMallPromotion() => $_has(19);
|
||||||
|
@$pb.TagNumber(20)
|
||||||
|
void clearMallPromotion() => clearField(20);
|
||||||
|
@$pb.TagNumber(20)
|
||||||
|
MallPromotion ensureMallPromotion() => $_ensure(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MallPromotion is an optional, version-gated set of low-impact entry points
|
||||||
|
/// layered on top of the existing mall_tab creative. Older clients safely
|
||||||
|
/// ignore this message.
|
||||||
|
class MallPromotion extends $pb.GeneratedMessage {
|
||||||
|
factory MallPromotion({
|
||||||
|
$core.bool? quoteCtaEnabled,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (quoteCtaEnabled != null) {
|
||||||
|
$result.quoteCtaEnabled = quoteCtaEnabled;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
MallPromotion._() : super();
|
||||||
|
factory MallPromotion.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory MallPromotion.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MallPromotion', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOB(1, _omitFieldNames ? '' : 'quoteCtaEnabled')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
MallPromotion clone() => MallPromotion()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
MallPromotion copyWith(void Function(MallPromotion) updates) => super.copyWith((message) => updates(message as MallPromotion)) as MallPromotion;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static MallPromotion create() => MallPromotion._();
|
||||||
|
MallPromotion createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<MallPromotion> createRepeated() => $pb.PbList<MallPromotion>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static MallPromotion getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<MallPromotion>(create);
|
||||||
|
static MallPromotion? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool get quoteCtaEnabled => $_getBF(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set quoteCtaEnabled($core.bool v) { $_setBool(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasQuoteCtaEnabled() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearQuoteCtaEnabled() => clearField(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReportAdEventRequest extends $pb.GeneratedMessage {
|
||||||
|
factory ReportAdEventRequest({
|
||||||
|
$core.String? eventName,
|
||||||
|
$core.String? placement,
|
||||||
|
$core.String? adId,
|
||||||
|
$core.String? trackingId,
|
||||||
|
$core.String? clickId,
|
||||||
|
$fixnum.Int64? clientTsMs,
|
||||||
|
$core.Map<$core.String, $core.String>? attrs,
|
||||||
|
$core.String? articleId,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (eventName != null) {
|
||||||
|
$result.eventName = eventName;
|
||||||
|
}
|
||||||
|
if (placement != null) {
|
||||||
|
$result.placement = placement;
|
||||||
|
}
|
||||||
|
if (adId != null) {
|
||||||
|
$result.adId = adId;
|
||||||
|
}
|
||||||
|
if (trackingId != null) {
|
||||||
|
$result.trackingId = trackingId;
|
||||||
|
}
|
||||||
|
if (clickId != null) {
|
||||||
|
$result.clickId = clickId;
|
||||||
|
}
|
||||||
|
if (clientTsMs != null) {
|
||||||
|
$result.clientTsMs = clientTsMs;
|
||||||
|
}
|
||||||
|
if (attrs != null) {
|
||||||
|
$result.attrs.addAll(attrs);
|
||||||
|
}
|
||||||
|
if (articleId != null) {
|
||||||
|
$result.articleId = articleId;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
ReportAdEventRequest._() : super();
|
||||||
|
factory ReportAdEventRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory ReportAdEventRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ReportAdEventRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'eventName')
|
||||||
|
..aOS(2, _omitFieldNames ? '' : 'placement')
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'adId')
|
||||||
|
..aOS(4, _omitFieldNames ? '' : 'trackingId')
|
||||||
|
..aOS(5, _omitFieldNames ? '' : 'clickId')
|
||||||
|
..aInt64(6, _omitFieldNames ? '' : 'clientTsMs')
|
||||||
|
..m<$core.String, $core.String>(7, _omitFieldNames ? '' : 'attrs', entryClassName: 'ReportAdEventRequest.AttrsEntry', keyFieldType: $pb.PbFieldType.OS, valueFieldType: $pb.PbFieldType.OS, packageName: const $pb.PackageName('jinzhi.v1'))
|
||||||
|
..aOS(8, _omitFieldNames ? '' : 'articleId')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ReportAdEventRequest clone() => ReportAdEventRequest()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ReportAdEventRequest copyWith(void Function(ReportAdEventRequest) updates) => super.copyWith((message) => updates(message as ReportAdEventRequest)) as ReportAdEventRequest;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ReportAdEventRequest create() => ReportAdEventRequest._();
|
||||||
|
ReportAdEventRequest createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<ReportAdEventRequest> createRepeated() => $pb.PbList<ReportAdEventRequest>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ReportAdEventRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ReportAdEventRequest>(create);
|
||||||
|
static ReportAdEventRequest? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get eventName => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set eventName($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasEventName() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearEventName() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.String get placement => $_getSZ(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set placement($core.String v) { $_setString(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasPlacement() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearPlacement() => clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get adId => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set adId($core.String v) { $_setString(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasAdId() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearAdId() => clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.String get trackingId => $_getSZ(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set trackingId($core.String v) { $_setString(3, v); }
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasTrackingId() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearTrackingId() => clearField(4);
|
||||||
|
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.String get clickId => $_getSZ(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
set clickId($core.String v) { $_setString(4, v); }
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool hasClickId() => $_has(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
void clearClickId() => clearField(5);
|
||||||
|
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$fixnum.Int64 get clientTsMs => $_getI64(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
set clientTsMs($fixnum.Int64 v) { $_setInt64(5, v); }
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.bool hasClientTsMs() => $_has(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
void clearClientTsMs() => clearField(6);
|
||||||
|
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.Map<$core.String, $core.String> get attrs => $_getMap(6);
|
||||||
|
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.String get articleId => $_getSZ(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
set articleId($core.String v) { $_setString(7, v); }
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
$core.bool hasArticleId() => $_has(7);
|
||||||
|
@$pb.TagNumber(8)
|
||||||
|
void clearArticleId() => clearField(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReportAdEventReply extends $pb.GeneratedMessage {
|
||||||
|
factory ReportAdEventReply({
|
||||||
|
$core.String? eventId,
|
||||||
|
$core.bool? accepted,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (eventId != null) {
|
||||||
|
$result.eventId = eventId;
|
||||||
|
}
|
||||||
|
if (accepted != null) {
|
||||||
|
$result.accepted = accepted;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
ReportAdEventReply._() : super();
|
||||||
|
factory ReportAdEventReply.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory ReportAdEventReply.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ReportAdEventReply', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'eventId')
|
||||||
|
..aOB(2, _omitFieldNames ? '' : 'accepted')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ReportAdEventReply clone() => ReportAdEventReply()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
ReportAdEventReply copyWith(void Function(ReportAdEventReply) updates) => super.copyWith((message) => updates(message as ReportAdEventReply)) as ReportAdEventReply;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ReportAdEventReply create() => ReportAdEventReply._();
|
||||||
|
ReportAdEventReply createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<ReportAdEventReply> createRepeated() => $pb.PbList<ReportAdEventReply>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ReportAdEventReply getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ReportAdEventReply>(create);
|
||||||
|
static ReportAdEventReply? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get eventId => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set eventId($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasEventId() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearEventId() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool get accepted => $_getBF(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set accepted($core.bool v) { $_setBool(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasAccepted() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearAccepted() => clearField(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdsServiceApi {
|
||||||
|
$pb.RpcClient _client;
|
||||||
|
AdsServiceApi(this._client);
|
||||||
|
|
||||||
|
$async.Future<GetAdReply> getAd($pb.ClientContext? ctx, GetAdRequest request) =>
|
||||||
|
_client.invoke<GetAdReply>(ctx, 'AdsService', 'GetAd', request, GetAdReply())
|
||||||
|
;
|
||||||
|
$async.Future<ListAdsReply> listAds($pb.ClientContext? ctx, ListAdsRequest request) =>
|
||||||
|
_client.invoke<ListAdsReply>(ctx, 'AdsService', 'ListAds', request, ListAdsReply())
|
||||||
|
;
|
||||||
|
$async.Future<ReportAdEventReply> reportAdEvent($pb.ClientContext? ctx, ReportAdEventRequest request) =>
|
||||||
|
_client.invoke<ReportAdEventReply>(ctx, 'AdsService', 'ReportAdEvent', request, ReportAdEventReply())
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||||
|
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: jinzhi/v1/ads.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
class AdOpenMode extends $pb.ProtobufEnum {
|
||||||
|
static const AdOpenMode AD_OPEN_MODE_UNSPECIFIED = AdOpenMode._(0, _omitEnumNames ? '' : 'AD_OPEN_MODE_UNSPECIFIED');
|
||||||
|
static const AdOpenMode AD_OPEN_MODE_EXTERNAL = AdOpenMode._(1, _omitEnumNames ? '' : 'AD_OPEN_MODE_EXTERNAL');
|
||||||
|
static const AdOpenMode AD_OPEN_MODE_IN_APP = AdOpenMode._(2, _omitEnumNames ? '' : 'AD_OPEN_MODE_IN_APP');
|
||||||
|
static const AdOpenMode AD_OPEN_MODE_NATIVE_APP = AdOpenMode._(3, _omitEnumNames ? '' : 'AD_OPEN_MODE_NATIVE_APP');
|
||||||
|
|
||||||
|
static const $core.List<AdOpenMode> values = <AdOpenMode> [
|
||||||
|
AD_OPEN_MODE_UNSPECIFIED,
|
||||||
|
AD_OPEN_MODE_EXTERNAL,
|
||||||
|
AD_OPEN_MODE_IN_APP,
|
||||||
|
AD_OPEN_MODE_NATIVE_APP,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.Map<$core.int, AdOpenMode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||||
|
static AdOpenMode? valueOf($core.int value) => _byValue[value];
|
||||||
|
|
||||||
|
const AdOpenMode._($core.int v, $core.String n) : super(v, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaType extends $pb.ProtobufEnum {
|
||||||
|
static const MediaType MEDIA_TYPE_UNSPECIFIED = MediaType._(0, _omitEnumNames ? '' : 'MEDIA_TYPE_UNSPECIFIED');
|
||||||
|
static const MediaType MEDIA_TYPE_IMAGE = MediaType._(1, _omitEnumNames ? '' : 'MEDIA_TYPE_IMAGE');
|
||||||
|
static const MediaType MEDIA_TYPE_GIF = MediaType._(2, _omitEnumNames ? '' : 'MEDIA_TYPE_GIF');
|
||||||
|
static const MediaType MEDIA_TYPE_VIDEO = MediaType._(3, _omitEnumNames ? '' : 'MEDIA_TYPE_VIDEO');
|
||||||
|
|
||||||
|
static const $core.List<MediaType> values = <MediaType> [
|
||||||
|
MEDIA_TYPE_UNSPECIFIED,
|
||||||
|
MEDIA_TYPE_IMAGE,
|
||||||
|
MEDIA_TYPE_GIF,
|
||||||
|
MEDIA_TYPE_VIDEO,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.Map<$core.int, MediaType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||||
|
static MediaType? valueOf($core.int value) => _byValue[value];
|
||||||
|
|
||||||
|
const MediaType._($core.int v, $core.String n) : super(v, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: jinzhi/v1/push.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
import 'push.pbenum.dart';
|
||||||
|
|
||||||
|
export 'push.pbenum.dart';
|
||||||
|
|
||||||
|
class RegisterPushDeviceRequest extends $pb.GeneratedMessage {
|
||||||
|
factory RegisterPushDeviceRequest({
|
||||||
|
$core.String? umengDeviceToken,
|
||||||
|
Platform? platform,
|
||||||
|
$core.String? appVersion,
|
||||||
|
$core.bool? permissionGranted,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (umengDeviceToken != null) {
|
||||||
|
$result.umengDeviceToken = umengDeviceToken;
|
||||||
|
}
|
||||||
|
if (platform != null) {
|
||||||
|
$result.platform = platform;
|
||||||
|
}
|
||||||
|
if (appVersion != null) {
|
||||||
|
$result.appVersion = appVersion;
|
||||||
|
}
|
||||||
|
if (permissionGranted != null) {
|
||||||
|
$result.permissionGranted = permissionGranted;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
RegisterPushDeviceRequest._() : super();
|
||||||
|
factory RegisterPushDeviceRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory RegisterPushDeviceRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RegisterPushDeviceRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'umengDeviceToken')
|
||||||
|
..e<Platform>(2, _omitFieldNames ? '' : 'platform', $pb.PbFieldType.OE, defaultOrMaker: Platform.PLATFORM_UNSPECIFIED, valueOf: Platform.valueOf, enumValues: Platform.values)
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'appVersion')
|
||||||
|
..aOB(4, _omitFieldNames ? '' : 'permissionGranted')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
RegisterPushDeviceRequest clone() => RegisterPushDeviceRequest()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
RegisterPushDeviceRequest copyWith(void Function(RegisterPushDeviceRequest) updates) => super.copyWith((message) => updates(message as RegisterPushDeviceRequest)) as RegisterPushDeviceRequest;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static RegisterPushDeviceRequest create() => RegisterPushDeviceRequest._();
|
||||||
|
RegisterPushDeviceRequest createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<RegisterPushDeviceRequest> createRepeated() => $pb.PbList<RegisterPushDeviceRequest>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static RegisterPushDeviceRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RegisterPushDeviceRequest>(create);
|
||||||
|
static RegisterPushDeviceRequest? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get umengDeviceToken => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set umengDeviceToken($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasUmengDeviceToken() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearUmengDeviceToken() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
Platform get platform => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set platform(Platform v) { setField(2, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasPlatform() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearPlatform() => clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get appVersion => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set appVersion($core.String v) { $_setString(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasAppVersion() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearAppVersion() => clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool get permissionGranted => $_getBF(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set permissionGranted($core.bool v) { $_setBool(3, v); }
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasPermissionGranted() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearPermissionGranted() => clearField(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegisterPushDeviceReply extends $pb.GeneratedMessage {
|
||||||
|
factory RegisterPushDeviceReply({
|
||||||
|
$core.String? deviceRecordId,
|
||||||
|
$core.bool? firstRegistration,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (deviceRecordId != null) {
|
||||||
|
$result.deviceRecordId = deviceRecordId;
|
||||||
|
}
|
||||||
|
if (firstRegistration != null) {
|
||||||
|
$result.firstRegistration = firstRegistration;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
RegisterPushDeviceReply._() : super();
|
||||||
|
factory RegisterPushDeviceReply.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory RegisterPushDeviceReply.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RegisterPushDeviceReply', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'deviceRecordId')
|
||||||
|
..aOB(2, _omitFieldNames ? '' : 'firstRegistration')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
RegisterPushDeviceReply clone() => RegisterPushDeviceReply()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
RegisterPushDeviceReply copyWith(void Function(RegisterPushDeviceReply) updates) => super.copyWith((message) => updates(message as RegisterPushDeviceReply)) as RegisterPushDeviceReply;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static RegisterPushDeviceReply create() => RegisterPushDeviceReply._();
|
||||||
|
RegisterPushDeviceReply createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<RegisterPushDeviceReply> createRepeated() => $pb.PbList<RegisterPushDeviceReply>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static RegisterPushDeviceReply getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RegisterPushDeviceReply>(create);
|
||||||
|
static RegisterPushDeviceReply? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get deviceRecordId => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set deviceRecordId($core.String v) { $_setString(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasDeviceRecordId() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearDeviceRecordId() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool get firstRegistration => $_getBF(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set firstRegistration($core.bool v) { $_setBool(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasFirstRegistration() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearFirstRegistration() => clearField(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 成对的 *_set 字段保留 proto3 bool 的局部更新语义。
|
||||||
|
class UpdatePushPreferenceRequest extends $pb.GeneratedMessage {
|
||||||
|
factory UpdatePushPreferenceRequest({
|
||||||
|
$core.bool? watchlistUpdatesEnabled,
|
||||||
|
$core.bool? watchlistUpdatesEnabledSet,
|
||||||
|
$core.bool? recallPushEnabled,
|
||||||
|
$core.bool? recallPushEnabledSet,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (watchlistUpdatesEnabled != null) {
|
||||||
|
$result.watchlistUpdatesEnabled = watchlistUpdatesEnabled;
|
||||||
|
}
|
||||||
|
if (watchlistUpdatesEnabledSet != null) {
|
||||||
|
$result.watchlistUpdatesEnabledSet = watchlistUpdatesEnabledSet;
|
||||||
|
}
|
||||||
|
if (recallPushEnabled != null) {
|
||||||
|
$result.recallPushEnabled = recallPushEnabled;
|
||||||
|
}
|
||||||
|
if (recallPushEnabledSet != null) {
|
||||||
|
$result.recallPushEnabledSet = recallPushEnabledSet;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
UpdatePushPreferenceRequest._() : super();
|
||||||
|
factory UpdatePushPreferenceRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory UpdatePushPreferenceRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UpdatePushPreferenceRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOB(1, _omitFieldNames ? '' : 'watchlistUpdatesEnabled')
|
||||||
|
..aOB(2, _omitFieldNames ? '' : 'watchlistUpdatesEnabledSet')
|
||||||
|
..aOB(3, _omitFieldNames ? '' : 'recallPushEnabled')
|
||||||
|
..aOB(4, _omitFieldNames ? '' : 'recallPushEnabledSet')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
UpdatePushPreferenceRequest clone() => UpdatePushPreferenceRequest()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
UpdatePushPreferenceRequest copyWith(void Function(UpdatePushPreferenceRequest) updates) => super.copyWith((message) => updates(message as UpdatePushPreferenceRequest)) as UpdatePushPreferenceRequest;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static UpdatePushPreferenceRequest create() => UpdatePushPreferenceRequest._();
|
||||||
|
UpdatePushPreferenceRequest createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<UpdatePushPreferenceRequest> createRepeated() => $pb.PbList<UpdatePushPreferenceRequest>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static UpdatePushPreferenceRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UpdatePushPreferenceRequest>(create);
|
||||||
|
static UpdatePushPreferenceRequest? _defaultInstance;
|
||||||
|
|
||||||
|
/// 沿用研值/研听契约名;在金值中表示“资产与关注内容更新”。
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool get watchlistUpdatesEnabled => $_getBF(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set watchlistUpdatesEnabled($core.bool v) { $_setBool(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasWatchlistUpdatesEnabled() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearWatchlistUpdatesEnabled() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool get watchlistUpdatesEnabledSet => $_getBF(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set watchlistUpdatesEnabledSet($core.bool v) { $_setBool(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasWatchlistUpdatesEnabledSet() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearWatchlistUpdatesEnabledSet() => clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool get recallPushEnabled => $_getBF(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set recallPushEnabled($core.bool v) { $_setBool(2, v); }
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasRecallPushEnabled() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearRecallPushEnabled() => clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool get recallPushEnabledSet => $_getBF(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set recallPushEnabledSet($core.bool v) { $_setBool(3, v); }
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasRecallPushEnabledSet() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearRecallPushEnabledSet() => clearField(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushPreferenceReply extends $pb.GeneratedMessage {
|
||||||
|
factory PushPreferenceReply({
|
||||||
|
$core.bool? watchlistUpdatesEnabled,
|
||||||
|
$core.bool? recallPushEnabled,
|
||||||
|
}) {
|
||||||
|
final $result = create();
|
||||||
|
if (watchlistUpdatesEnabled != null) {
|
||||||
|
$result.watchlistUpdatesEnabled = watchlistUpdatesEnabled;
|
||||||
|
}
|
||||||
|
if (recallPushEnabled != null) {
|
||||||
|
$result.recallPushEnabled = recallPushEnabled;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
PushPreferenceReply._() : super();
|
||||||
|
factory PushPreferenceReply.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||||
|
factory PushPreferenceReply.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushPreferenceReply', package: const $pb.PackageName(_omitMessageNames ? '' : 'jinzhi.v1'), createEmptyInstance: create)
|
||||||
|
..aOB(1, _omitFieldNames ? '' : 'watchlistUpdatesEnabled')
|
||||||
|
..aOB(2, _omitFieldNames ? '' : 'recallPushEnabled')
|
||||||
|
..hasRequiredFields = false
|
||||||
|
;
|
||||||
|
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
PushPreferenceReply clone() => PushPreferenceReply()..mergeFromMessage(this);
|
||||||
|
@$core.Deprecated(
|
||||||
|
'Using this can add significant overhead to your binary. '
|
||||||
|
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||||
|
'Will be removed in next major version')
|
||||||
|
PushPreferenceReply copyWith(void Function(PushPreferenceReply) updates) => super.copyWith((message) => updates(message as PushPreferenceReply)) as PushPreferenceReply;
|
||||||
|
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static PushPreferenceReply create() => PushPreferenceReply._();
|
||||||
|
PushPreferenceReply createEmptyInstance() => create();
|
||||||
|
static $pb.PbList<PushPreferenceReply> createRepeated() => $pb.PbList<PushPreferenceReply>();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static PushPreferenceReply getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PushPreferenceReply>(create);
|
||||||
|
static PushPreferenceReply? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool get watchlistUpdatesEnabled => $_getBF(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set watchlistUpdatesEnabled($core.bool v) { $_setBool(0, v); }
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasWatchlistUpdatesEnabled() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearWatchlistUpdatesEnabled() => clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool get recallPushEnabled => $_getBF(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set recallPushEnabled($core.bool v) { $_setBool(1, v); }
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasRecallPushEnabled() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearRecallPushEnabled() => clearField(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||||
|
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Generated code. Do not modify.
|
||||||
|
// source: jinzhi/v1/push.proto
|
||||||
|
//
|
||||||
|
// @dart = 2.12
|
||||||
|
|
||||||
|
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||||
|
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||||
|
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||||
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
|
/// Platform 标识 U-Push token 所属原生平台。
|
||||||
|
class Platform extends $pb.ProtobufEnum {
|
||||||
|
static const Platform PLATFORM_UNSPECIFIED = Platform._(0, _omitEnumNames ? '' : 'PLATFORM_UNSPECIFIED');
|
||||||
|
static const Platform PLATFORM_IOS = Platform._(1, _omitEnumNames ? '' : 'PLATFORM_IOS');
|
||||||
|
static const Platform PLATFORM_ANDROID = Platform._(2, _omitEnumNames ? '' : 'PLATFORM_ANDROID');
|
||||||
|
|
||||||
|
static const $core.List<Platform> values = <Platform> [
|
||||||
|
PLATFORM_UNSPECIFIED,
|
||||||
|
PLATFORM_IOS,
|
||||||
|
PLATFORM_ANDROID,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.Map<$core.int, Platform> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||||
|
static Platform? valueOf($core.int value) => _byValue[value];
|
||||||
|
|
||||||
|
const Platform._($core.int v, $core.String n) : super(v, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||||
+1
-1
@@ -2,7 +2,7 @@ name: jinzhi_flutter_app
|
|||||||
description: "金值 - 贵金属资产管家"
|
description: "金值 - 贵金属资产管家"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.3.0+1
|
version: 1.1.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/network/api_error_message.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/network/kratos_error_interceptor.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/errors/errors.pb.dart' as errors;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('decodes a protobuf error body before it reaches the UI', () async {
|
||||||
|
final dio = Dio()
|
||||||
|
..httpClientAdapter = _ErrorAdapter(
|
||||||
|
errors.Status(
|
||||||
|
code: 401,
|
||||||
|
reason: 'INVALID_CODE',
|
||||||
|
message: '验证码错误',
|
||||||
|
).writeToBuffer(),
|
||||||
|
)
|
||||||
|
..interceptors.add(KratosErrorInterceptor());
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
dio.delete<void>(
|
||||||
|
'/api/v1/me',
|
||||||
|
options: Options(responseType: ResponseType.bytes),
|
||||||
|
),
|
||||||
|
throwsA(
|
||||||
|
isA<DioException>().having(
|
||||||
|
(error) => kratosDisplayMessage(error, fallback: '注销失败,请重试'),
|
||||||
|
'display message',
|
||||||
|
'验证码错误',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorAdapter implements HttpClientAdapter {
|
||||||
|
_ErrorAdapter(this._body);
|
||||||
|
|
||||||
|
final List<int> _body;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ResponseBody> fetch(
|
||||||
|
RequestOptions options,
|
||||||
|
Stream<Uint8List>? requestStream,
|
||||||
|
Future<void>? cancelFuture,
|
||||||
|
) async => ResponseBody.fromBytes(
|
||||||
|
_body,
|
||||||
|
401,
|
||||||
|
headers: {
|
||||||
|
'content-type': ['application/x-protobuf'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool force = false}) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/auth/data/auth_api.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('keeps the verified one-key login contract', () async {
|
||||||
|
String? requestToken;
|
||||||
|
final dio = Dio()
|
||||||
|
..interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
requestToken = pb.VerifyOneKeyLoginRequest.fromBuffer(
|
||||||
|
options.data as List<int>,
|
||||||
|
).loginToken;
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: pb.VerifySmsCodeReply(
|
||||||
|
accessToken: 'access-token',
|
||||||
|
user: pb.UserInfo(
|
||||||
|
id: 'user-1',
|
||||||
|
phone: '13800138000',
|
||||||
|
createdAt: '2026-09-08T00:00:00Z',
|
||||||
|
),
|
||||||
|
).writeToBuffer(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await AuthApi(dio).verifyOneKeyLogin('umeng-token');
|
||||||
|
|
||||||
|
expect(requestToken, 'umeng-token');
|
||||||
|
expect(result.accessToken, 'access-token');
|
||||||
|
expect(result.user.phone, '13800138000');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
test('restores a token by loading the real profile', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
|
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||||
|
expect(auth.token, 'token');
|
||||||
|
expect(auth.user.id, 'user-1');
|
||||||
|
expect(auth.user.phone, '13800138000');
|
||||||
|
expect(fixture.preferences.getString(storageUserId), 'user-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes the server account before clearing local identity', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
|
await fixture.controller.deleteAccount('123456');
|
||||||
|
|
||||||
|
expect(fixture.deletedWithCode, '123456');
|
||||||
|
expect(fixture.controller.state.value, isA<GuestAuthState>());
|
||||||
|
expect(await fixture.identity.readJwt(), isNull);
|
||||||
|
expect(fixture.preferences.getString(storageUserId), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the local session when server deletion fails', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
fixture.failDeletion = true;
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
fixture.controller.deleteAccount('123456'),
|
||||||
|
throwsA(isA<DioException>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fixture.controller.state.value, isA<LoggedInAuthState>());
|
||||||
|
expect(await fixture.identity.readJwt(), 'token');
|
||||||
|
expect(fixture.preferences.getString(storageUserId), 'user-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears an unauthorized restored session', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('expired');
|
||||||
|
fixture.profileUnauthorized = true;
|
||||||
|
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
|
expect(fixture.controller.state.value, isA<GuestAuthState>());
|
||||||
|
expect(await fixture.identity.readJwt(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps a valid token and cached profile on a network failure', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
await fixture.preferences.setString('jinzhi_mock_logged_in', '13900139000');
|
||||||
|
fixture.failProfile = true;
|
||||||
|
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
|
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||||
|
expect(auth.user.id, isEmpty);
|
||||||
|
expect(auth.user.phone, '13900139000');
|
||||||
|
expect(await fixture.identity.readJwt(), 'token');
|
||||||
|
expect(fixture.preferences.getString(storageUserPhone), '13900139000');
|
||||||
|
expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the verified one-key login state shape', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
|
||||||
|
await fixture.controller.verifyOneKeyLogin('umeng-token');
|
||||||
|
|
||||||
|
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||||
|
expect(auth.token, 'one-key-token');
|
||||||
|
expect(auth.user.phone, '13800138000');
|
||||||
|
expect(await fixture.identity.readJwt(), 'one-key-token');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Fixture {
|
||||||
|
_Fixture({
|
||||||
|
required this.preferences,
|
||||||
|
required this.identity,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SharedPreferences preferences;
|
||||||
|
final SecureIdentityStorage identity;
|
||||||
|
final AuthController controller;
|
||||||
|
bool failDeletion = false;
|
||||||
|
bool failProfile = false;
|
||||||
|
bool profileUnauthorized = false;
|
||||||
|
String? deletedWithCode;
|
||||||
|
|
||||||
|
static Future<_Fixture> create() async {
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final identity = SecureIdentityStorage(
|
||||||
|
preferences,
|
||||||
|
secureStorage: _MemorySecureStorage(),
|
||||||
|
);
|
||||||
|
late _Fixture fixture;
|
||||||
|
final dio = Dio()
|
||||||
|
..interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
if (options.path == '/api/v1/me' && options.method == 'GET') {
|
||||||
|
if (fixture.failProfile) {
|
||||||
|
handler.reject(DioException(requestOptions: options));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fixture.profileUnauthorized) {
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: options,
|
||||||
|
response: Response<void>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 401,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: pb.ProfileReply(
|
||||||
|
user: pb.UserInfo(
|
||||||
|
id: 'user-1',
|
||||||
|
phone: '13800138000',
|
||||||
|
createdAt: '2026-09-08T00:00:00Z',
|
||||||
|
),
|
||||||
|
).writeToBuffer(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.path == '/api/v1/me' && options.method == 'DELETE') {
|
||||||
|
fixture.deletedWithCode = pb.DeleteAccountRequest.fromBuffer(
|
||||||
|
options.data as List<int>,
|
||||||
|
).smsCode;
|
||||||
|
if (fixture.failDeletion) {
|
||||||
|
handler.reject(DioException(requestOptions: options));
|
||||||
|
} else {
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: const <int>[],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.path == '/api/v1/auth/onekey/verify') {
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: pb.VerifySmsCodeReply(
|
||||||
|
accessToken: 'one-key-token',
|
||||||
|
user: pb.UserInfo(phone: '13800138000'),
|
||||||
|
).writeToBuffer(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final controller = AuthController(
|
||||||
|
dio,
|
||||||
|
identityStorage: identity,
|
||||||
|
preferences: preferences,
|
||||||
|
autoBootstrap: false,
|
||||||
|
);
|
||||||
|
fixture = _Fixture(
|
||||||
|
preferences: preferences,
|
||||||
|
identity: identity,
|
||||||
|
controller: controller,
|
||||||
|
);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MemorySecureStorage implements SecureKeyValueStorage {
|
||||||
|
final Map<String, String> _values = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String key) async => _values.remove(key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read(String key) async => _values[key];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> write(String key, String value) async {
|
||||||
|
_values[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dar
|
|||||||
void main() {
|
void main() {
|
||||||
test('preserves Jinzhi attribution while syncing H5 login', () {
|
test('preserves Jinzhi attribution while syncing H5 login', () {
|
||||||
final arguments = buildEnterWebMallBridgeArguments(
|
final arguments = buildEnterWebMallBridgeArguments(
|
||||||
|
config: const MallTabConfig(
|
||||||
|
enabled: true,
|
||||||
|
url: 'https://mall.example.com/',
|
||||||
|
adId: 'latest-ad',
|
||||||
|
trackingId: 'latest-tracking',
|
||||||
|
clickId: 'latest-click',
|
||||||
|
entryRequest: MallEntryRequest.quoteRetailGold,
|
||||||
|
),
|
||||||
currentUri: Uri.parse(
|
currentUri: Uri.parse(
|
||||||
'https://mall.example.com/?from=jinzhi&source_app=jinzhi'
|
'https://mall.example.com/?from=jinzhi&source_app=jinzhi'
|
||||||
'&source_entry=market_quote&placement=mall_tab&hook_type=quote_cta'
|
'&source_entry=market_quote&placement=mall_tab&hook_type=quote_cta'
|
||||||
@@ -15,11 +23,6 @@ void main() {
|
|||||||
authToken: 'mall-token',
|
authToken: 'mall-token',
|
||||||
scene: 'web_mall',
|
scene: 'web_mall',
|
||||||
sourceApp: 'jinzhi',
|
sourceApp: 'jinzhi',
|
||||||
attribution: const MallClickAttribution(
|
|
||||||
adId: 'latest-ad',
|
|
||||||
trackingId: 'latest-tracking',
|
|
||||||
clickId: 'latest-click',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(arguments['from'], 'jinzhi');
|
expect(arguments['from'], 'jinzhi');
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('keeps click attribution and reports the full generated event', () async {
|
||||||
|
pb.ReportAdEventRequest? clickEvent;
|
||||||
|
final dio = Dio()
|
||||||
|
..interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
if (options.path == '/api/v1/ads') {
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: pb.GetAdReply(
|
||||||
|
enabled: true,
|
||||||
|
ad: pb.AdCreative(
|
||||||
|
adId: 'ad-1',
|
||||||
|
h5Url: 'https://mall.example.com',
|
||||||
|
trackingId: 'tracking-1',
|
||||||
|
openMode: pb.AdOpenMode.AD_OPEN_MODE_IN_APP,
|
||||||
|
),
|
||||||
|
).writeToBuffer(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.path == '/api/v1/ads/events') {
|
||||||
|
final event = pb.ReportAdEventRequest.fromBuffer(
|
||||||
|
options.data as List<int>,
|
||||||
|
);
|
||||||
|
if (event.eventName == 'ad_click') clickEvent = event;
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: pb.ReportAdEventReply().writeToBuffer(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final controller = MallTabConfigController(dio, autoRefresh: false);
|
||||||
|
await controller.refresh();
|
||||||
|
|
||||||
|
final clicked = await controller.recordClick(
|
||||||
|
request: MallEntryRequest.quoteRetailGold,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clicked.clickId, isNotEmpty);
|
||||||
|
expect(controller.state.clickId, clicked.clickId);
|
||||||
|
expect(clickEvent?.clickId, clicked.clickId);
|
||||||
|
expect(clickEvent?.attrs['source_app'], 'jinzhi');
|
||||||
|
expect(clickEvent?.attrs['source_entry'], 'market_quote');
|
||||||
|
expect(clickEvent?.attrs['hook_type'], 'quote_cta');
|
||||||
|
expect(clickEvent?.attrs['entry_scene'], 'retail');
|
||||||
|
expect(clickEvent?.attrs['entry_metal'], 'au');
|
||||||
|
expect(clickEvent?.attrs['open_mode'], 'in_app');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('builds the resident mall URL with Jinzhi attribution and theme', () {
|
test('builds the resident mall URL with Jinzhi attribution and theme', () {
|
||||||
@@ -102,6 +103,27 @@ void main() {
|
|||||||
expect(config.entryTipTitle, '新人礼:¥360 铂金订购券');
|
expect(config.entryTipTitle, '新人礼:¥360 铂金订购券');
|
||||||
expect(config.entryTipBody, '点击这里进商城');
|
expect(config.entryTipBody, '点击这里进商城');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses generated mall promotion and open-mode fields', () {
|
||||||
|
final response = pb.GetAdReply(
|
||||||
|
enabled: true,
|
||||||
|
ad: pb.AdCreative(
|
||||||
|
adId: 'ad-1',
|
||||||
|
title: '会员商城',
|
||||||
|
h5Url: 'https://mall.example.com',
|
||||||
|
trackingId: 'tracking-1',
|
||||||
|
openMode: pb.AdOpenMode.AD_OPEN_MODE_NATIVE_APP,
|
||||||
|
mallPromotion: pb.MallPromotion(quoteCtaEnabled: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final config = parseMallTabConfigResponse(response.writeToBuffer());
|
||||||
|
|
||||||
|
expect(config.isVisible, isTrue);
|
||||||
|
expect(config.openMode, 'native_app');
|
||||||
|
expect(config.quoteCtaEnabled, isTrue);
|
||||||
|
expect(config.allowedHosts, {'mall.example.com'});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _stringField(int tag, String value) =>
|
List<int> _stringField(int tag, String value) =>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:jinzhi_flutter_app/features/web/presentation/web_page.dart';
|
import 'package:jinzhi_flutter_app/features/web/presentation/web_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('accepts only http and https URLs with a host', () {
|
test('accepts HTTPS and rejects unsafe or remote HTTP URLs', () {
|
||||||
expect(
|
expect(
|
||||||
parseSafeWebUri('https://h5.example.com/page')?.host,
|
parseSafeWebUri('https://h5.example.com/page')?.host,
|
||||||
'h5.example.com',
|
'h5.example.com',
|
||||||
);
|
);
|
||||||
expect(parseSafeWebUri('mailto:test@example.com'), isNull);
|
expect(parseSafeWebUri('mailto:test@example.com'), isNull);
|
||||||
expect(parseSafeWebUri('javascript:alert(1)'), isNull);
|
expect(parseSafeWebUri('javascript:alert(1)'), isNull);
|
||||||
|
expect(parseSafeWebUri('http://example.com/page'), isNull);
|
||||||
expect(parseSafeWebUri(''), isNull);
|
expect(parseSafeWebUri(''), isNull);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user