fix:对齐金值的报名验证,商城入口逻辑对齐

This commit is contained in:
jingyun committed 2026-09-08 10:56:33 +08:00
1 parent 3b089e2205
commit 7b1852d9a6
20 files changed
+682 -116

No files matched your search

+4 -1
View File
@@ -4,7 +4,10 @@ class AppConfig {
factory AppConfig.fromEnvironment() {
const url = String.fromEnvironment(
'JINZHI_API_BASE_URL',
defaultValue: 'https://yanting.api.hnsiderui.cn',
// defaultValue: 'http://localhost:8088',
defaultValue: 'https://jinzhi.api.yqzhice.top',
// defaultValue: 'http://8.153.17.57',可测试ip
);
return const AppConfig(apiBaseUrl: url);
}
+129 -40
View File
@@ -9,13 +9,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../features/mall/application/mall_entry_controller.dart';
import '../../features/mall/application/mall_entry_promo_store.dart';
import '../../features/mall/application/mall_tab_config.dart';
import '../../features/mall/presentation/widgets/mall_entry_promo.dart';
import '../../features/ads/application/startup_ad_service.dart';
import '../../features/web/application/mall_bridge_state.dart';
import '../../features/web/application/mall_web_host_controller.dart';
import '../../features/web/presentation/mall_web_host.dart';
import '../theme/app_icons.dart';
import '../widgets/app_toast.dart';
import 'app_router.dart';
import '../storage/storage_service.dart';
class TabShell extends ConsumerStatefulWidget {
const TabShell({
@@ -47,6 +51,10 @@ class _TabShellState extends ConsumerState<TabShell>
late final AnimationController _mallTransition;
late final CurvedAnimation _mallProgress;
late final ProviderSubscription<MallTabConfig> _mallConfigSubscription;
late final MallEntryPromoStore _mallEntryPromoStore;
late bool _mallEntryTipConsumed;
late bool _mallHomeBadgeSeen;
bool _showMallHomeBadgeForPresentation = false;
bool _userScrollSession = false;
ScrollDirection _userScrollDirection = ScrollDirection.idle;
double _directionAccum = 0;
@@ -57,6 +65,9 @@ class _TabShellState extends ConsumerState<TabShell>
@override
void initState() {
super.initState();
_mallEntryPromoStore = MallEntryPromoStore(StorageService.to.preferences);
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
MallWebHostController.instance.state.addListener(_syncMallHost);
MallBridgeController.instance.state.addListener(_refreshMallDock);
_minimizeController = AnimationController(
@@ -130,10 +141,22 @@ class _TabShellState extends ConsumerState<TabShell>
final state = MallWebHostController.instance.state.value;
if (!mounted) return;
final changed = _mallActive != state.visible;
final showHomeBadge =
changed &&
state.visible &&
!_mallHomeBadgeSeen &&
ref.read(mallTabConfigProvider).showsEntryBadge;
setState(() {
_mallPresented |= state.url != null;
_mallActive = state.visible;
if (changed) {
_showMallHomeBadgeForPresentation = showHomeBadge;
if (showHomeBadge) _mallHomeBadgeSeen = true;
}
});
if (showHomeBadge) {
unawaited(_mallEntryPromoStore.markHomeBadgeSeen());
}
if (changed) {
if (state.visible) {
_mallTransition.forward();
@@ -148,6 +171,8 @@ class _TabShellState extends ConsumerState<TabShell>
}
Future<void> _openMall(MallTabConfig config) async {
await _consumeMallEntryTip(config);
if (!mounted) return;
final result = await ref
.read(mallEntryControllerProvider)
.open(context, initialConfig: config);
@@ -159,6 +184,20 @@ class _TabShellState extends ConsumerState<TabShell>
}
}
Future<void> _consumeMallEntryTip(MallTabConfig config) async {
if (!config.showsEntryTip || _mallEntryTipConsumed) return;
setState(() => _mallEntryTipConsumed = true);
await _mallEntryPromoStore.markMallOpened();
}
bool get _isPrimaryTabRoot => switch (widget.location) {
AppRoutes.assets ||
AppRoutes.market ||
AppRoutes.news ||
AppRoutes.profile => true,
_ => false,
};
@override
void didUpdateWidget(covariant TabShell oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -173,6 +212,7 @@ class _TabShellState extends ConsumerState<TabShell>
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final mall = ref.watch(mallTabConfigProvider);
final startupAdResolved = ref.watch(startupAdResolvedProvider);
final isQiyuKeyboardActive =
MallWebHostController.instance.state.value.isQiyuKeyboardActive;
final header = _headerForPath(widget.location);
@@ -312,6 +352,14 @@ class _TabShellState extends ConsumerState<TabShell>
minimize: _minimizeFactor,
currentIndex: _index,
mall: mall,
showMallEntryTip:
startupAdResolved &&
_isPrimaryTabRoot &&
!_mallActive &&
!_mallEntryTipConsumed &&
mall.showsEntryTip,
showMallHomeBadge:
_showMallHomeBadgeForPresentation && mall.showsEntryBadge,
onTabTap: (index) => widget.navigationShell.goBranch(
index,
initialLocation: index == _index,
@@ -437,6 +485,8 @@ class _MorphingNavigationDock extends StatelessWidget {
required this.minimize,
required this.currentIndex,
required this.mall,
required this.showMallEntryTip,
required this.showMallHomeBadge,
required this.onTabTap,
required this.onMallTap,
});
@@ -445,6 +495,8 @@ class _MorphingNavigationDock extends StatelessWidget {
final Animation<double> minimize;
final int currentIndex;
final MallTabConfig mall;
final bool showMallEntryTip;
final bool showMallHomeBadge;
final ValueChanged<int> onTabTap;
final VoidCallback onMallTap;
@@ -545,6 +597,7 @@ class _MorphingNavigationDock extends StatelessWidget {
opacity: (1 - t * 2).clamp(0.0, 1.0),
child: _MallLauncherButton(
mall: mall,
showPromoTip: showMallEntryTip,
onTap: onMallTap,
),
),
@@ -570,6 +623,9 @@ class _MorphingNavigationDock extends StatelessWidget {
.tabs[index]
.$1,
),
promoBadgeIndex: 0,
promoBadgeVisible: showMallHomeBadge,
promoBadgeLabel: mall.entryBadgeText,
),
),
),
@@ -591,37 +647,48 @@ class _MorphingNavigationDock extends StatelessWidget {
}
class _MallLauncherButton extends StatelessWidget {
const _MallLauncherButton({required this.mall, required this.onTap});
const _MallLauncherButton({
required this.mall,
required this.showPromoTip,
required this.onTap,
});
final MallTabConfig mall;
final bool showPromoTip;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return Semantics(
button: true,
label: mall.title,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
return MallEntryPromoTooltip(
visible: showPromoTip,
title: mall.entryTipTitle,
body: mall.entryTipBody,
onCardTap: onTap,
child: Semantics(
button: true,
label: mall.title,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
),
),
),
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Center(
child: Icon(
Icons.shopping_bag,
size: 25,
color: cs.primaryForeground,
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Center(
child: Icon(
Icons.shopping_bag,
size: 25,
color: cs.primaryForeground,
),
),
),
),
@@ -651,6 +718,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
required this.selectedIndex,
required this.onTap,
this.minimize = 0,
this.promoBadgeIndex,
this.promoBadgeVisible = false,
this.promoBadgeLabel = '',
});
static const double _inset = 6;
@@ -660,6 +730,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onTap;
final double minimize;
final int? promoBadgeIndex;
final bool promoBadgeVisible;
final String promoBadgeLabel;
@override
Widget build(BuildContext context) {
@@ -676,6 +749,7 @@ class _ExpandingDockTabStrip extends StatelessWidget {
final indicatorLeft = _inset + safeIndex * compactWidth;
return Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 260),
@@ -697,13 +771,19 @@ class _ExpandingDockTabStrip extends StatelessWidget {
duration: const Duration(milliseconds: 260),
curve: Curves.easeOutCubic,
width: index == safeIndex ? selectedWidth : compactWidth,
child: _ExpandingDockTab(
item: items[index],
selected: index == safeIndex,
labelFactor: index == safeIndex
? (1 - minimize).clamp(0.0, 1.0)
: 0,
onTap: () => onTap(index),
child: MallEntryPromoBadge(
visible: index == promoBadgeIndex && promoBadgeVisible,
label: promoBadgeLabel,
top: -2,
right: 0,
child: _ExpandingDockTab(
item: items[index],
selected: index == safeIndex,
labelFactor: index == safeIndex
? (1 - minimize).clamp(0.0, 1.0)
: 0,
onTap: () => onTap(index),
),
),
),
],
@@ -859,19 +939,28 @@ class _MallDockSurface extends StatelessWidget {
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(height / 2),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: DecoratedBox(
decoration: BoxDecoration(
color: cs.background.withValues(alpha: 0.82),
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
// Clip only the frosted background. The mall promotion badge is
// intentionally positioned above the pill and must remain visible.
child: ClipRRect(
borderRadius: BorderRadius.circular(height / 2),
border: Border.all(color: cs.border.withValues(alpha: 0.8)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: DecoratedBox(
decoration: BoxDecoration(
color: cs.background.withValues(alpha: 0.82),
borderRadius: BorderRadius.circular(height / 2),
border: Border.all(color: cs.border.withValues(alpha: 0.8)),
),
),
),
),
child: child,
),
),
Positioned.fill(child: child),
],
),
);
}
+2
View File
@@ -8,6 +8,8 @@ const storageJinzhiPurityPresets = 'jinzhi_purity_presets';
const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode';
const storageJinzhiMockLoggedIn = 'jinzhi_mock_logged_in';
const storagePushPermissionPrompted = 'push_permission_prompted';
const storageMallEntryPromoOpened = 'jinzhi_mall_entry_promo_opened';
const storageMallHomeBadgeSeen = 'jinzhi_mall_home_badge_seen';
// Previous builds could persist credentials in SharedPreferences. These keys
// exist only for one-time migration into secure storage.
+6 -3
View File
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
@@ -16,7 +17,9 @@ class AuthApi {
Future<int> sendSmsCode(String phone) async {
final response = await _dio.post<List<int>>(
'/api/v1/auth/sms/send',
data: _ProtoWriter.stringField(1, phone),
// Dio may JSON-encode a List<int> as `[10,11,...]`. The API expects
// the raw protobuf bytes, so preserve the binary payload explicitly.
data: Uint8List.fromList(_ProtoWriter.stringField(1, phone)),
options: protobufOptions(),
);
return _ProtoMessage(response.data ?? const <int>[]).integer(1);
@@ -25,10 +28,10 @@ class AuthApi {
Future<AuthLoginResult> verifySmsCode(String phone, String code) async {
final response = await _dio.post<List<int>>(
'/api/v1/auth/sms/verify',
data: [
data: Uint8List.fromList([
..._ProtoWriter.stringField(1, phone),
..._ProtoWriter.stringField(2, code),
],
]),
options: protobufOptions(),
);
final reply = _ProtoMessage(response.data ?? const <int>[]);
@@ -0,0 +1,23 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../../../common/storage/storage_keys.dart';
class MallEntryPromoStore {
const MallEntryPromoStore(this._preferences);
final SharedPreferences _preferences;
bool get hasOpenedMall =>
_preferences.getBool(storageMallEntryPromoOpened) ?? false;
bool get hasSeenHomeBadge =>
_preferences.getBool(storageMallHomeBadgeSeen) ?? false;
Future<void> markMallOpened() async {
await _preferences.setBool(storageMallEntryPromoOpened, true);
}
Future<void> markHomeBadgeSeen() async {
await _preferences.setBool(storageMallHomeBadgeSeen, true);
}
}
@@ -19,7 +19,10 @@ class MallTabConfig {
this.adId = '',
this.trackingId = '',
this.openMode = 'in_app',
this.badgeText = '',
this.entryPromoEnabled = false,
this.entryBadgeText = '',
this.entryTipTitle = '',
this.entryTipBody = '',
});
const MallTabConfig.disabled() : this(enabled: false, url: '');
@@ -30,10 +33,19 @@ class MallTabConfig {
final String adId;
final String trackingId;
final String openMode;
final String badgeText;
final bool entryPromoEnabled;
final String entryBadgeText;
final String entryTipTitle;
final String entryTipBody;
bool get isVisible => enabled && initialUri != null;
bool get opensInApp => openMode == 'in_app';
bool get showsEntryBadge =>
isVisible && entryPromoEnabled && entryBadgeText.trim().isNotEmpty;
bool get showsEntryTip =>
isVisible &&
entryPromoEnabled &&
(entryTipTitle.trim().isNotEmpty || entryTipBody.trim().isNotEmpty);
Uri? get initialUri => attributedUri(clickId: '');
@@ -91,24 +103,8 @@ class MallTabConfigController extends StateNotifier<MallTabConfig> {
queryParameters: const {'placement': 'mall_tab'},
options: protobufOptions(),
);
final reply = _ProtoMessage(response.data ?? const []).message(2);
final enabled = _ProtoMessage(response.data ?? const []).integer(1) != 0;
final trackedUrl = reply.string(14);
final h5Url = reply.string(9);
final url = _validUrl(trackedUrl) ?? _validUrl(h5Url);
if (!enabled || url == null) {
state = const MallTabConfig.disabled();
return;
}
state = MallTabConfig(
enabled: true,
url: url,
title: _nonEmpty(reply.string(4), '商城'),
adId: reply.string(1),
trackingId: reply.string(10),
openMode: _openMode(reply.integer(15)),
badgeText: reply.string(17).trim(),
);
state = parseMallTabConfigResponse(response.data ?? const []);
if (!state.isVisible) return;
await _report('ad_impression');
} catch (_) {
state = const MallTabConfig.disabled();
@@ -216,6 +212,30 @@ String _openMode(int value) => switch (value) {
_ => 'external',
};
/// Parses the protobuf reply returned by `/api/v1/ads?placement=mall_tab`.
MallTabConfig parseMallTabConfigResponse(List<int> bytes) {
final response = _ProtoMessage(bytes);
final reply = response.message(2);
final trackedUrl = reply.string(14);
final h5Url = reply.string(9);
final url = _validUrl(trackedUrl) ?? _validUrl(h5Url);
if (response.integer(1) == 0 || url == null) {
return const MallTabConfig.disabled();
}
return MallTabConfig(
enabled: true,
url: url,
title: _nonEmpty(reply.string(4), '商城'),
adId: reply.string(1),
trackingId: reply.string(10),
openMode: _openMode(reply.integer(15)),
entryPromoEnabled: reply.integer(16) != 0,
entryBadgeText: reply.string(17).trim(),
entryTipTitle: reply.string(18).trim(),
entryTipBody: reply.string(19).trim(),
);
}
class _ProtoMessage {
_ProtoMessage(this._bytes);
final List<int> _bytes;
@@ -0,0 +1,345 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
String _withChinesePunctuation(String value) {
const replacements = <String, String>{
':': ':',
',': ',',
';': ';',
'!': '!',
'?': '?',
'(': '(',
')': ')',
'[': '【',
']': '】',
'¥': '¥',
};
return value.split('').map((char) => replacements[char] ?? char).join();
}
class MallEntryPromoTooltip extends StatefulWidget {
const MallEntryPromoTooltip({
super.key,
required this.visible,
required this.title,
required this.body,
required this.onCardTap,
required this.child,
});
final bool visible;
final String title;
final String body;
final VoidCallback onCardTap;
final Widget child;
@override
State<MallEntryPromoTooltip> createState() => _MallEntryPromoTooltipState();
}
class _MallEntryPromoTooltipState extends State<MallEntryPromoTooltip> {
static const _cardWidth = 236.0;
static const _cardHeight = 115.0;
static const _anchorToCardBottom = 54.0;
static const _targetCenterToCardRight = 23.0;
static const _cardRightInset = 22.0;
final _targetKey = GlobalKey();
OverlayEntry? _overlayEntry;
bool _routeIsCurrent = true;
@override
void initState() {
super.initState();
_scheduleVisibilitySync();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_routeIsCurrent = ModalRoute.of(context)?.isCurrent ?? true;
_scheduleVisibilitySync();
}
@override
void didUpdateWidget(covariant MallEntryPromoTooltip oldWidget) {
super.didUpdateWidget(oldWidget);
_scheduleVisibilitySync();
}
void _scheduleVisibilitySync() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (widget.visible && _routeIsCurrent) {
_showOverlay();
} else {
_removeOverlay();
}
});
}
void _showOverlay() {
final current = _overlayEntry;
if (current != null) {
current.markNeedsBuild();
return;
}
final entry = OverlayEntry(builder: _buildOverlay);
_overlayEntry = entry;
Overlay.of(context, rootOverlay: true).insert(entry);
}
void _removeOverlay() {
final entry = _overlayEntry;
if (entry == null) return;
_overlayEntry = null;
entry
..remove()
..dispose();
}
@override
void dispose() {
_removeOverlay();
super.dispose();
}
@override
Widget build(BuildContext context) {
return KeyedSubtree(key: _targetKey, child: widget.child);
}
Widget _buildOverlay(BuildContext overlayContext) {
final targetContext = _targetKey.currentContext;
final targetBox = targetContext?.findRenderObject();
if (targetContext == null ||
targetBox is! RenderBox ||
!targetBox.attached) {
return const SizedBox.shrink();
}
final overlayBox = Overlay.of(
targetContext,
rootOverlay: true,
).context.findRenderObject();
if (overlayBox is! RenderBox || !overlayBox.attached) {
return const SizedBox.shrink();
}
final targetCenter = targetBox.localToGlobal(
targetBox.size.center(Offset.zero),
ancestor: overlayBox,
);
final title = _withChinesePunctuation(widget.title.trim());
final body = _withChinesePunctuation(widget.body.trim());
return Positioned.fill(
child: LayoutBuilder(
key: const ValueKey('mall-entry-promo-pass-through-layer'),
builder: (context, constraints) {
final overlaySize = Size(constraints.maxWidth, constraints.maxHeight);
final cardWidth = math.min(
_cardWidth,
math.max(0.0, overlaySize.width - 40),
);
final desiredCardLeft =
targetCenter.dx + _targetCenterToCardRight - cardWidth;
final maximumCardLeft = math.max(
20.0,
overlaySize.width - cardWidth - _cardRightInset,
);
final cardLeft = desiredCardLeft
.clamp(20.0, maximumCardLeft)
.toDouble();
final desiredCardTop =
targetCenter.dy - _anchorToCardBottom - _cardHeight;
final minimumTop = MediaQuery.paddingOf(context).top + 16;
final maximumTop = math.max(
minimumTop,
overlaySize.height - _cardHeight - 20,
);
final cardTop = desiredCardTop
.clamp(minimumTop, maximumTop)
.toDouble();
final cardBottom = cardTop + _cardHeight;
final pointerLeft = (targetCenter.dx - 9)
.clamp(0.0, overlaySize.width - 18)
.toDouble();
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: cardLeft,
top: cardTop,
width: cardWidth,
height: _cardHeight,
child: Semantics(
button: true,
label: '进入商城',
child: GestureDetector(
key: const ValueKey('mall-entry-promo-card-action'),
behavior: HitTestBehavior.opaque,
onTap: widget.onCardTap,
child: DecoratedBox(
key: const ValueKey('mall-entry-promo-card'),
decoration: BoxDecoration(
color: const Color(0xE6FFFFFF),
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
offset: Offset(0, 8),
blurRadius: 20,
),
],
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/navigation/mall_entry_promo_gift.svg',
key: const ValueKey('mall-entry-promo-gift'),
width: 45,
height: 40,
fit: BoxFit.fill,
),
const SizedBox(height: 4),
if (title.isNotEmpty)
SizedBox(
width: double.infinity,
height: 22,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
title,
maxLines: 1,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.375,
),
),
),
),
if (title.isNotEmpty && body.isNotEmpty)
const SizedBox(height: 4),
if (body.isNotEmpty)
Text(
body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0x99000000),
fontSize: 12,
fontWeight: FontWeight.w400,
height: 17 / 12,
),
),
],
),
),
),
),
),
),
Positioned(
left: pointerLeft,
top: cardBottom - 1,
width: 18,
height: 16,
child: IgnorePointer(
child: SvgPicture.asset(
'assets/navigation/mall_entry_promo_pointer.svg',
key: const ValueKey('mall-entry-promo-pointer'),
fit: BoxFit.fill,
),
),
),
],
);
},
),
);
}
}
class MallEntryPromoBadge extends StatelessWidget {
const MallEntryPromoBadge({
super.key,
required this.visible,
required this.label,
required this.child,
this.top = -4,
this.right = -20,
});
final bool visible;
final String label;
final Widget child;
final double top;
final double right;
static const _singleCharacterWidth = 30.0;
static const _badgeHeight = 18.0;
@override
Widget build(BuildContext context) {
final text = label.trim();
if (!visible || text.isEmpty) return child;
final isSingleCharacter = text.runes.length == 1;
return Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
top: top,
right: isSingleCharacter ? right : right + 8,
child: IgnorePointer(
child: Badge(
key: const ValueKey('mall-entry-promo-badge'),
backgroundColor: const Color(0xFFE7473C),
textColor: Colors.white,
largeSize: isSingleCharacter ? _badgeHeight : null,
textStyle: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
height: 1,
),
padding: isSingleCharacter
? EdgeInsets.zero
: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
label: isSingleCharacter
? SizedBox(
key: const ValueKey('mall-entry-promo-badge-label'),
width: _singleCharacterWidth,
height: _badgeHeight,
child: Center(
child: Text(
text,
maxLines: 1,
textAlign: TextAlign.center,
strutStyle: const StrutStyle(
height: 1,
leading: 0,
forceStrutHeight: true,
),
),
),
)
: Text(
text,
key: const ValueKey('mall-entry-promo-badge-label'),
maxLines: 1,
),
),
),
),
],
);
}
}
@@ -49,32 +49,17 @@ class UpgradeInfo {
}
class UpdateApiClient {
UpdateApiClient(
this._dio, {
required String apiPathPrefix,
required String updatePackageName,
}) : _apiPathPrefix = _normalizePrefix(apiPathPrefix),
_updatePackageName = updatePackageName.trim();
UpdateApiClient(this._dio);
final Dio _dio;
final String _apiPathPrefix;
final String _updatePackageName;
Options get _options {
final options = protobufOptions();
if (_updatePackageName.isEmpty) return options;
return options.copyWith(
headers: {...?options.headers, 'pkg': _updatePackageName},
);
}
Future<UpgradeInfo> checkUpgrade() async {
final response = await _dio.post<List<int>>(
_path('/sys/upgrade'),
'/sys/upgrade',
// Uint8List ensures Dio sends the empty protobuf as 0 bytes instead of
// JSON-encoding an empty Dart List as `[]` (Content-Length: 2).
data: Uint8List(0), // jinzhi.v1.GetUpgradeRequest
options: _options,
options: protobufOptions(),
);
return UpgradeInfo.fromProtobuf(response.data ?? const <int>[]);
}
@@ -84,33 +69,18 @@ class UpdateApiClient {
required String source,
}) async {
await _dio.post<List<int>>(
_path('/active/upgrade/click'),
'/active/upgrade/click',
data: _ProtoWriter()
.string(1, buildId)
.string(2, source)
.toBytes(), // jinzhi.v1.UpgradeClickRequest
options: _options,
options: protobufOptions(),
);
}
String _path(String endpoint) => '$_apiPathPrefix$endpoint';
static String _normalizePrefix(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty || trimmed == '/') return '';
final withLeadingSlash = trimmed.startsWith('/') ? trimmed : '/$trimmed';
return withLeadingSlash.endsWith('/')
? withLeadingSlash.substring(0, withLeadingSlash.length - 1)
: withLeadingSlash;
}
}
final updateApiClientProvider = Provider<UpdateApiClient>(
(ref) => UpdateApiClient(
ref.watch(dioProvider),
apiPathPrefix: ref.watch(appConfigProvider).apiPathPrefix,
updatePackageName: ref.watch(appConfigProvider).updatePackageName,
),
(ref) => UpdateApiClient(ref.watch(dioProvider)),
);
class _ProtoFields {
@@ -58,7 +58,7 @@ class _MallWebHostState extends ConsumerState<MallWebHost>
statusBarBrightness: Brightness.light,
);
static const _webFilePickerChannel = MethodChannel(
'com.jinzhi.app/web_file_picker',
'com.jinzhi.butler/web_file_picker',
);
late final DWebViewController _controller;