fix:对齐金值的报名验证,商城入口逻辑对齐
This commit is contained in:
1 parent
3b089e2205
commit
7b1852d9a6
20 files changed
+682
-116
No files matched your search
@@ -33,7 +33,7 @@ fun requireSigningProperty(name: String): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.jinzhi.app"
|
namespace = "com.jinzhi.butler"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -56,10 +56,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// Keep the registered Jinzhi application identity. One-key login and
|
||||||
// Temporary U-Verify test identity: the Yanting AppKey/login secret
|
// push providers validate the package-and-certificate pair.
|
||||||
// loaded from assets/env/.env is registered against this package.
|
applicationId = "com.jinzhi.butler"
|
||||||
applicationId = "com.yanting.read"
|
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.jinzhi.app
|
package com.jinzhi.butler
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.jinzhi.app
|
package com.jinzhi.butler
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@@ -16,7 +16,7 @@ import java.io.File
|
|||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val WEB_FILE_PICKER_CHANNEL = "com.jinzhi.app/web_file_picker"
|
private const val WEB_FILE_PICKER_CHANNEL = "com.jinzhi.butler/web_file_picker"
|
||||||
private const val WEB_FILE_PICKER_REQUEST_CODE = 9814
|
private const val WEB_FILE_PICKER_REQUEST_CODE = 9814
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="26.9924" viewBox="0 0 20 26.9924" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="装饰星星">
|
||||||
|
<path id="Vector" d="M7.7027 26.5261L8.41064 24.9055C9.02993 23.4731 10.1634 22.3247 11.5875 21.6867L13.5356 20.8223C14.1548 20.5473 14.1548 19.6467 13.5356 19.3709L11.6488 18.534C10.1882 17.879 9.03458 16.6885 8.42596 15.2079L7.70835 13.4808C7.65191 13.3389 7.55418 13.2173 7.42782 13.1315C7.30147 13.0458 7.15229 13 6.9996 13C6.84691 13 6.69773 13.0458 6.57137 13.1315C6.44501 13.2173 6.34729 13.3389 6.29085 13.4808L5.57404 15.2079C4.96542 16.6885 3.81184 17.879 2.35121 18.534L0.464436 19.3717C-0.154812 19.6459 -0.154812 20.5473 0.464436 20.8223L2.41168 21.6867C3.83568 22.3245 4.9691 23.4726 5.58855 24.9047L6.29649 26.5253C6.35471 26.6638 6.45248 26.7819 6.57756 26.865C6.70263 26.9481 6.84944 26.9924 6.9996 26.9924C7.14975 26.9924 7.29656 26.9481 7.42164 26.865C7.54671 26.7819 7.64448 26.6646 7.7027 26.5261Z" fill="white"/>
|
||||||
|
<path id="Vector_2" d="M16.4666 7.6867L16.7133 7.12119C17.1465 6.12115 17.9381 5.31928 18.9324 4.87314L19.6918 4.53443C19.7838 4.49211 19.8617 4.4243 19.9163 4.33903C19.971 4.25376 20 4.15462 20 4.05335C20 3.95208 19.971 3.85294 19.9163 3.76767C19.8617 3.6824 19.7838 3.61459 19.6918 3.57226L18.9754 3.25354C17.9551 2.79573 17.1494 1.9639 16.7243 0.929556L16.4706 0.319085C16.433 0.224921 16.3681 0.144177 16.2842 0.0872928C16.2003 0.0304083 16.1013 0 16 0C15.8986 0 15.7996 0.0304083 15.7157 0.0872928C15.6318 0.144177 15.5669 0.224921 15.5294 0.319085L15.2756 0.928557C14.8507 1.96328 14.045 2.7955 13.0245 3.25354L12.3072 3.57327C12.2154 3.61571 12.1378 3.68351 12.0833 3.76867C12.0289 3.85383 12 3.95278 12 4.05385C12 4.15491 12.0289 4.25387 12.0833 4.33903C12.1378 4.42419 12.2154 4.49199 12.3072 4.53443L13.0665 4.87214C14.0614 5.31832 14.8533 6.1206 15.2866 7.12119L15.5334 7.6867C15.7132 8.10034 16.2867 8.10034 16.4666 7.6867Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="72" height="64" viewBox="0 0 72 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Frame">
|
||||||
|
<path id="Vector" d="M52 35.0088V49.0088C52 49.5393 51.7893 50.048 51.4142 50.4231C51.0391 50.7981 50.5304 51.0088 50 51.0088H22C21.4696 51.0088 20.9609 50.7981 20.5858 50.4231C20.2107 50.048 20 49.5393 20 49.0088V35.0088H52ZM41 13.0088C42.181 13.0086 43.3429 13.3072 44.3775 13.8768C45.4121 14.4464 46.2858 15.2685 46.9171 16.2666C47.5485 17.2647 47.9171 18.4063 47.9886 19.5851C48.0601 20.764 47.8322 21.9418 47.326 23.0088H54C54.5304 23.0088 55.0391 23.2196 55.4142 23.5946C55.7893 23.9697 56 24.4784 56 25.0088V31.0088C56 31.5393 55.7893 32.048 55.4142 32.4231C55.0391 32.7981 54.5304 33.0088 54 33.0088H18C17.4696 33.0088 16.9609 32.7981 16.5858 32.4231C16.2107 32.048 16 31.5393 16 31.0088V25.0088C16 24.4784 16.2107 23.9697 16.5858 23.5946C16.9609 23.2196 17.4696 23.0088 18 23.0088H24.674C23.9615 21.5122 23.8033 19.8113 24.2274 18.2088C24.6515 16.6064 25.6306 15.2065 26.9902 14.2583C28.3498 13.3102 30.0018 12.8752 31.6521 13.031C33.3023 13.1867 34.8438 13.923 36.002 15.1088C36.6522 14.4423 37.4296 13.9129 38.2881 13.5522C39.1466 13.1915 40.0688 13.0067 41 13.0088ZM31 17.0088C30.2294 17.0087 29.4883 17.3051 28.9303 17.8366C28.3723 18.3681 28.0403 19.094 28.003 19.8637C27.9657 20.6334 28.226 21.3879 28.73 21.9709C29.234 22.5538 29.943 22.9205 30.71 22.9948L31 23.0088H34V20.0088C34 19.2631 33.7222 18.5441 33.2208 17.992C32.7194 17.44 32.0303 17.0944 31.288 17.0228L31 17.0088ZM41 17.0088L40.71 17.0228C40.0178 17.0901 39.3704 17.3957 38.8786 17.8875C38.3869 18.3793 38.0812 19.0266 38.014 19.7188L38 20.0088V23.0088H41L41.288 22.9948C42.0301 22.9229 42.7187 22.5772 43.2198 22.0252C43.7209 21.4732 43.9985 20.7544 43.9985 20.0088C43.9985 19.2633 43.7209 18.5445 43.2198 17.9925C42.7187 17.4404 42.0301 17.0948 41.288 17.0228L41 17.0088Z" fill="black"/>
|
||||||
|
<path id="Vector_2" d="M7.36867 18.1835L7.954 16.8435C8.46603 15.6592 9.40317 14.7097 10.5807 14.1822L12.1913 13.4675C12.7033 13.2402 12.7033 12.4955 12.1913 12.2675L10.6313 11.5755C9.42367 11.034 8.46988 10.0497 7.96667 8.82555L7.37333 7.39755C7.32667 7.28024 7.24587 7.17964 7.1414 7.10877C7.03692 7.03789 6.91358 7 6.78733 7C6.66109 7 6.53775 7.03789 6.43327 7.10877C6.3288 7.17964 6.248 7.28024 6.20133 7.39755L5.60867 8.82555C5.10546 10.0497 4.15166 11.034 2.944 11.5755L1.384 12.2682C0.872 12.4949 0.872 13.2402 1.384 13.4675L2.994 14.1822C4.17138 14.7096 5.1085 15.6588 5.62067 16.8429L6.206 18.1829C6.25414 18.2973 6.33497 18.395 6.43839 18.4637C6.5418 18.5324 6.66319 18.569 6.78733 18.569C6.91148 18.569 7.03287 18.5324 7.13628 18.4637C7.23969 18.395 7.32053 18.298 7.36867 18.1835Z" fill="black"/>
|
||||||
|
<path id="Vector_3" d="M65.9606 51.2578L66.2899 50.5031C66.868 49.1686 67.9243 48.0985 69.2512 47.5031L70.2646 47.0512C70.3873 46.9947 70.4914 46.9042 70.5643 46.7904C70.6372 46.6766 70.6759 46.5443 70.6759 46.4092C70.6759 46.274 70.6372 46.1417 70.5643 46.0279C70.4914 45.9141 70.3873 45.8236 70.2646 45.7671L69.3086 45.3418C67.947 44.7309 66.8718 43.6208 66.3046 42.2405L65.9659 41.4258C65.9158 41.3002 65.8292 41.1924 65.7173 41.1165C65.6053 41.0406 65.4732 41 65.3379 41C65.2026 41 65.0705 41.0406 64.9585 41.1165C64.8466 41.1924 64.76 41.3002 64.7099 41.4258L64.3712 42.2391C63.8043 43.62 62.729 44.7306 61.3672 45.3418L60.4099 45.7685C60.2875 45.8251 60.1839 45.9156 60.1112 46.0292C60.0386 46.1429 60 46.2749 60 46.4098C60 46.5447 60.0386 46.6767 60.1112 46.7904C60.1839 46.904 60.2875 46.9945 60.4099 47.0512L61.4232 47.5018C62.7509 48.0972 63.8077 49.1679 64.3859 50.5031L64.7152 51.2578C64.9552 51.8098 65.7206 51.8098 65.9606 51.2578Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path id="Vector 1" d="M8.74286 16L0 0H18L8.74286 16Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 243 B |
@@ -300,10 +300,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -355,10 +359,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -491,14 +499,14 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = F38D68G8T6;
|
DEVELOPMENT_TEAM = 3QMFZJWPLS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yanting.read;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.butler;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -678,14 +686,14 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = F38D68G8T6;
|
DEVELOPMENT_TEAM = 3QMFZJWPLS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yanting.read;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.butler;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -705,14 +713,14 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = F38D68G8T6;
|
DEVELOPMENT_TEAM = 3QMFZJWPLS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yanting.read;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.butler;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ class AppConfig {
|
|||||||
factory AppConfig.fromEnvironment() {
|
factory AppConfig.fromEnvironment() {
|
||||||
const url = String.fromEnvironment(
|
const url = String.fromEnvironment(
|
||||||
'JINZHI_API_BASE_URL',
|
'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);
|
return const AppConfig(apiBaseUrl: url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
import '../../features/mall/application/mall_entry_controller.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/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_bridge_state.dart';
|
||||||
import '../../features/web/application/mall_web_host_controller.dart';
|
import '../../features/web/application/mall_web_host_controller.dart';
|
||||||
import '../../features/web/presentation/mall_web_host.dart';
|
import '../../features/web/presentation/mall_web_host.dart';
|
||||||
import '../theme/app_icons.dart';
|
import '../theme/app_icons.dart';
|
||||||
import '../widgets/app_toast.dart';
|
import '../widgets/app_toast.dart';
|
||||||
import 'app_router.dart';
|
import 'app_router.dart';
|
||||||
|
import '../storage/storage_service.dart';
|
||||||
|
|
||||||
class TabShell extends ConsumerStatefulWidget {
|
class TabShell extends ConsumerStatefulWidget {
|
||||||
const TabShell({
|
const TabShell({
|
||||||
@@ -47,6 +51,10 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
late final AnimationController _mallTransition;
|
late final AnimationController _mallTransition;
|
||||||
late final CurvedAnimation _mallProgress;
|
late final CurvedAnimation _mallProgress;
|
||||||
late final ProviderSubscription<MallTabConfig> _mallConfigSubscription;
|
late final ProviderSubscription<MallTabConfig> _mallConfigSubscription;
|
||||||
|
late final MallEntryPromoStore _mallEntryPromoStore;
|
||||||
|
late bool _mallEntryTipConsumed;
|
||||||
|
late bool _mallHomeBadgeSeen;
|
||||||
|
bool _showMallHomeBadgeForPresentation = false;
|
||||||
bool _userScrollSession = false;
|
bool _userScrollSession = false;
|
||||||
ScrollDirection _userScrollDirection = ScrollDirection.idle;
|
ScrollDirection _userScrollDirection = ScrollDirection.idle;
|
||||||
double _directionAccum = 0;
|
double _directionAccum = 0;
|
||||||
@@ -57,6 +65,9 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_mallEntryPromoStore = MallEntryPromoStore(StorageService.to.preferences);
|
||||||
|
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
|
||||||
|
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
|
||||||
MallWebHostController.instance.state.addListener(_syncMallHost);
|
MallWebHostController.instance.state.addListener(_syncMallHost);
|
||||||
MallBridgeController.instance.state.addListener(_refreshMallDock);
|
MallBridgeController.instance.state.addListener(_refreshMallDock);
|
||||||
_minimizeController = AnimationController(
|
_minimizeController = AnimationController(
|
||||||
@@ -130,10 +141,22 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
final state = MallWebHostController.instance.state.value;
|
final state = MallWebHostController.instance.state.value;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final changed = _mallActive != state.visible;
|
final changed = _mallActive != state.visible;
|
||||||
|
final showHomeBadge =
|
||||||
|
changed &&
|
||||||
|
state.visible &&
|
||||||
|
!_mallHomeBadgeSeen &&
|
||||||
|
ref.read(mallTabConfigProvider).showsEntryBadge;
|
||||||
setState(() {
|
setState(() {
|
||||||
_mallPresented |= state.url != null;
|
_mallPresented |= state.url != null;
|
||||||
_mallActive = state.visible;
|
_mallActive = state.visible;
|
||||||
|
if (changed) {
|
||||||
|
_showMallHomeBadgeForPresentation = showHomeBadge;
|
||||||
|
if (showHomeBadge) _mallHomeBadgeSeen = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (showHomeBadge) {
|
||||||
|
unawaited(_mallEntryPromoStore.markHomeBadgeSeen());
|
||||||
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
if (state.visible) {
|
if (state.visible) {
|
||||||
_mallTransition.forward();
|
_mallTransition.forward();
|
||||||
@@ -148,6 +171,8 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openMall(MallTabConfig config) async {
|
Future<void> _openMall(MallTabConfig config) async {
|
||||||
|
await _consumeMallEntryTip(config);
|
||||||
|
if (!mounted) return;
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(mallEntryControllerProvider)
|
.read(mallEntryControllerProvider)
|
||||||
.open(context, initialConfig: config);
|
.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
|
@override
|
||||||
void didUpdateWidget(covariant TabShell oldWidget) {
|
void didUpdateWidget(covariant TabShell oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@@ -173,6 +212,7 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = ShadTheme.of(context).colorScheme;
|
final cs = ShadTheme.of(context).colorScheme;
|
||||||
final mall = ref.watch(mallTabConfigProvider);
|
final mall = ref.watch(mallTabConfigProvider);
|
||||||
|
final startupAdResolved = ref.watch(startupAdResolvedProvider);
|
||||||
final isQiyuKeyboardActive =
|
final isQiyuKeyboardActive =
|
||||||
MallWebHostController.instance.state.value.isQiyuKeyboardActive;
|
MallWebHostController.instance.state.value.isQiyuKeyboardActive;
|
||||||
final header = _headerForPath(widget.location);
|
final header = _headerForPath(widget.location);
|
||||||
@@ -312,6 +352,14 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
minimize: _minimizeFactor,
|
minimize: _minimizeFactor,
|
||||||
currentIndex: _index,
|
currentIndex: _index,
|
||||||
mall: mall,
|
mall: mall,
|
||||||
|
showMallEntryTip:
|
||||||
|
startupAdResolved &&
|
||||||
|
_isPrimaryTabRoot &&
|
||||||
|
!_mallActive &&
|
||||||
|
!_mallEntryTipConsumed &&
|
||||||
|
mall.showsEntryTip,
|
||||||
|
showMallHomeBadge:
|
||||||
|
_showMallHomeBadgeForPresentation && mall.showsEntryBadge,
|
||||||
onTabTap: (index) => widget.navigationShell.goBranch(
|
onTabTap: (index) => widget.navigationShell.goBranch(
|
||||||
index,
|
index,
|
||||||
initialLocation: index == _index,
|
initialLocation: index == _index,
|
||||||
@@ -437,6 +485,8 @@ class _MorphingNavigationDock extends StatelessWidget {
|
|||||||
required this.minimize,
|
required this.minimize,
|
||||||
required this.currentIndex,
|
required this.currentIndex,
|
||||||
required this.mall,
|
required this.mall,
|
||||||
|
required this.showMallEntryTip,
|
||||||
|
required this.showMallHomeBadge,
|
||||||
required this.onTabTap,
|
required this.onTabTap,
|
||||||
required this.onMallTap,
|
required this.onMallTap,
|
||||||
});
|
});
|
||||||
@@ -445,6 +495,8 @@ class _MorphingNavigationDock extends StatelessWidget {
|
|||||||
final Animation<double> minimize;
|
final Animation<double> minimize;
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
final MallTabConfig mall;
|
final MallTabConfig mall;
|
||||||
|
final bool showMallEntryTip;
|
||||||
|
final bool showMallHomeBadge;
|
||||||
final ValueChanged<int> onTabTap;
|
final ValueChanged<int> onTabTap;
|
||||||
final VoidCallback onMallTap;
|
final VoidCallback onMallTap;
|
||||||
|
|
||||||
@@ -545,6 +597,7 @@ class _MorphingNavigationDock extends StatelessWidget {
|
|||||||
opacity: (1 - t * 2).clamp(0.0, 1.0),
|
opacity: (1 - t * 2).clamp(0.0, 1.0),
|
||||||
child: _MallLauncherButton(
|
child: _MallLauncherButton(
|
||||||
mall: mall,
|
mall: mall,
|
||||||
|
showPromoTip: showMallEntryTip,
|
||||||
onTap: onMallTap,
|
onTap: onMallTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -570,6 +623,9 @@ class _MorphingNavigationDock extends StatelessWidget {
|
|||||||
.tabs[index]
|
.tabs[index]
|
||||||
.$1,
|
.$1,
|
||||||
),
|
),
|
||||||
|
promoBadgeIndex: 0,
|
||||||
|
promoBadgeVisible: showMallHomeBadge,
|
||||||
|
promoBadgeLabel: mall.entryBadgeText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -591,37 +647,48 @@ class _MorphingNavigationDock extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MallLauncherButton 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 MallTabConfig mall;
|
||||||
|
final bool showPromoTip;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = ShadTheme.of(context).colorScheme;
|
final cs = ShadTheme.of(context).colorScheme;
|
||||||
return Semantics(
|
return MallEntryPromoTooltip(
|
||||||
button: true,
|
visible: showPromoTip,
|
||||||
label: mall.title,
|
title: mall.entryTipTitle,
|
||||||
child: DecoratedBox(
|
body: mall.entryTipBody,
|
||||||
decoration: BoxDecoration(
|
onCardTap: onTap,
|
||||||
shape: BoxShape.circle,
|
child: Semantics(
|
||||||
gradient: LinearGradient(
|
button: true,
|
||||||
begin: Alignment.topLeft,
|
label: mall.title,
|
||||||
end: Alignment.bottomRight,
|
child: DecoratedBox(
|
||||||
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Material(
|
||||||
child: Material(
|
color: Colors.transparent,
|
||||||
color: Colors.transparent,
|
shape: const CircleBorder(),
|
||||||
shape: const CircleBorder(),
|
clipBehavior: Clip.antiAlias,
|
||||||
clipBehavior: Clip.antiAlias,
|
child: InkWell(
|
||||||
child: InkWell(
|
onTap: onTap,
|
||||||
onTap: onTap,
|
child: Center(
|
||||||
child: Center(
|
child: Icon(
|
||||||
child: Icon(
|
Icons.shopping_bag,
|
||||||
Icons.shopping_bag,
|
size: 25,
|
||||||
size: 25,
|
color: cs.primaryForeground,
|
||||||
color: cs.primaryForeground,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -651,6 +718,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
|||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.minimize = 0,
|
this.minimize = 0,
|
||||||
|
this.promoBadgeIndex,
|
||||||
|
this.promoBadgeVisible = false,
|
||||||
|
this.promoBadgeLabel = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
static const double _inset = 6;
|
static const double _inset = 6;
|
||||||
@@ -660,6 +730,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
|||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
final ValueChanged<int> onTap;
|
final ValueChanged<int> onTap;
|
||||||
final double minimize;
|
final double minimize;
|
||||||
|
final int? promoBadgeIndex;
|
||||||
|
final bool promoBadgeVisible;
|
||||||
|
final String promoBadgeLabel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -676,6 +749,7 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
|||||||
final indicatorLeft = _inset + safeIndex * compactWidth;
|
final indicatorLeft = _inset + safeIndex * compactWidth;
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 260),
|
duration: const Duration(milliseconds: 260),
|
||||||
@@ -697,13 +771,19 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 260),
|
duration: const Duration(milliseconds: 260),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
width: index == safeIndex ? selectedWidth : compactWidth,
|
width: index == safeIndex ? selectedWidth : compactWidth,
|
||||||
child: _ExpandingDockTab(
|
child: MallEntryPromoBadge(
|
||||||
item: items[index],
|
visible: index == promoBadgeIndex && promoBadgeVisible,
|
||||||
selected: index == safeIndex,
|
label: promoBadgeLabel,
|
||||||
labelFactor: index == safeIndex
|
top: -2,
|
||||||
? (1 - minimize).clamp(0.0, 1.0)
|
right: 0,
|
||||||
: 0,
|
child: _ExpandingDockTab(
|
||||||
onTap: () => onTap(index),
|
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(
|
child: Stack(
|
||||||
borderRadius: BorderRadius.circular(height / 2),
|
clipBehavior: Clip.none,
|
||||||
child: BackdropFilter(
|
children: [
|
||||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
Positioned.fill(
|
||||||
child: DecoratedBox(
|
// Clip only the frosted background. The mall promotion badge is
|
||||||
decoration: BoxDecoration(
|
// intentionally positioned above the pill and must remain visible.
|
||||||
color: cs.background.withValues(alpha: 0.82),
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(height / 2),
|
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),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const storageJinzhiPurityPresets = 'jinzhi_purity_presets';
|
|||||||
const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode';
|
const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode';
|
||||||
const storageJinzhiMockLoggedIn = 'jinzhi_mock_logged_in';
|
const storageJinzhiMockLoggedIn = 'jinzhi_mock_logged_in';
|
||||||
const storagePushPermissionPrompted = 'push_permission_prompted';
|
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
|
// Previous builds could persist credentials in SharedPreferences. These keys
|
||||||
// exist only for one-time migration into secure storage.
|
// exist only for one-time migration into secure storage.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@ 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',
|
||||||
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(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
return _ProtoMessage(response.data ?? const <int>[]).integer(1);
|
return _ProtoMessage(response.data ?? const <int>[]).integer(1);
|
||||||
@@ -25,10 +28,10 @@ class AuthApi {
|
|||||||
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: [
|
data: Uint8List.fromList([
|
||||||
..._ProtoWriter.stringField(1, phone),
|
..._ProtoWriter.stringField(1, phone),
|
||||||
..._ProtoWriter.stringField(2, code),
|
..._ProtoWriter.stringField(2, code),
|
||||||
],
|
]),
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
final reply = _ProtoMessage(response.data ?? const <int>[]);
|
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.adId = '',
|
||||||
this.trackingId = '',
|
this.trackingId = '',
|
||||||
this.openMode = 'in_app',
|
this.openMode = 'in_app',
|
||||||
this.badgeText = '',
|
this.entryPromoEnabled = false,
|
||||||
|
this.entryBadgeText = '',
|
||||||
|
this.entryTipTitle = '',
|
||||||
|
this.entryTipBody = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const MallTabConfig.disabled() : this(enabled: false, url: '');
|
const MallTabConfig.disabled() : this(enabled: false, url: '');
|
||||||
@@ -30,10 +33,19 @@ class MallTabConfig {
|
|||||||
final String adId;
|
final String adId;
|
||||||
final String trackingId;
|
final String trackingId;
|
||||||
final String openMode;
|
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 isVisible => enabled && initialUri != null;
|
||||||
bool get opensInApp => openMode == 'in_app';
|
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: '');
|
Uri? get initialUri => attributedUri(clickId: '');
|
||||||
|
|
||||||
@@ -91,24 +103,8 @@ class MallTabConfigController extends StateNotifier<MallTabConfig> {
|
|||||||
queryParameters: const {'placement': 'mall_tab'},
|
queryParameters: const {'placement': 'mall_tab'},
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
final reply = _ProtoMessage(response.data ?? const []).message(2);
|
state = parseMallTabConfigResponse(response.data ?? const []);
|
||||||
final enabled = _ProtoMessage(response.data ?? const []).integer(1) != 0;
|
if (!state.isVisible) return;
|
||||||
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(),
|
|
||||||
);
|
|
||||||
await _report('ad_impression');
|
await _report('ad_impression');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
state = const MallTabConfig.disabled();
|
state = const MallTabConfig.disabled();
|
||||||
@@ -216,6 +212,30 @@ String _openMode(int value) => switch (value) {
|
|||||||
_ => 'external',
|
_ => '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 {
|
class _ProtoMessage {
|
||||||
_ProtoMessage(this._bytes);
|
_ProtoMessage(this._bytes);
|
||||||
final List<int> _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 {
|
class UpdateApiClient {
|
||||||
UpdateApiClient(
|
UpdateApiClient(this._dio);
|
||||||
this._dio, {
|
|
||||||
required String apiPathPrefix,
|
|
||||||
required String updatePackageName,
|
|
||||||
}) : _apiPathPrefix = _normalizePrefix(apiPathPrefix),
|
|
||||||
_updatePackageName = updatePackageName.trim();
|
|
||||||
|
|
||||||
final Dio _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 {
|
Future<UpgradeInfo> checkUpgrade() async {
|
||||||
final response = await _dio.post<List<int>>(
|
final response = await _dio.post<List<int>>(
|
||||||
_path('/sys/upgrade'),
|
'/sys/upgrade',
|
||||||
// Uint8List ensures Dio sends the empty protobuf as 0 bytes instead of
|
// Uint8List ensures Dio sends the empty protobuf as 0 bytes instead of
|
||||||
// JSON-encoding an empty Dart List as `[]` (Content-Length: 2).
|
// JSON-encoding an empty Dart List as `[]` (Content-Length: 2).
|
||||||
data: Uint8List(0), // jinzhi.v1.GetUpgradeRequest
|
data: Uint8List(0), // jinzhi.v1.GetUpgradeRequest
|
||||||
options: _options,
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
return UpgradeInfo.fromProtobuf(response.data ?? const <int>[]);
|
return UpgradeInfo.fromProtobuf(response.data ?? const <int>[]);
|
||||||
}
|
}
|
||||||
@@ -84,33 +69,18 @@ class UpdateApiClient {
|
|||||||
required String source,
|
required String source,
|
||||||
}) async {
|
}) async {
|
||||||
await _dio.post<List<int>>(
|
await _dio.post<List<int>>(
|
||||||
_path('/active/upgrade/click'),
|
'/active/upgrade/click',
|
||||||
data: _ProtoWriter()
|
data: _ProtoWriter()
|
||||||
.string(1, buildId)
|
.string(1, buildId)
|
||||||
.string(2, source)
|
.string(2, source)
|
||||||
.toBytes(), // jinzhi.v1.UpgradeClickRequest
|
.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>(
|
final updateApiClientProvider = Provider<UpdateApiClient>(
|
||||||
(ref) => UpdateApiClient(
|
(ref) => UpdateApiClient(ref.watch(dioProvider)),
|
||||||
ref.watch(dioProvider),
|
|
||||||
apiPathPrefix: ref.watch(appConfigProvider).apiPathPrefix,
|
|
||||||
updatePackageName: ref.watch(appConfigProvider).updatePackageName,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
class _ProtoFields {
|
class _ProtoFields {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class _MallWebHostState extends ConsumerState<MallWebHost>
|
|||||||
statusBarBrightness: Brightness.light,
|
statusBarBrightness: Brightness.light,
|
||||||
);
|
);
|
||||||
static const _webFilePickerChannel = MethodChannel(
|
static const _webFilePickerChannel = MethodChannel(
|
||||||
'com.jinzhi.app/web_file_picker',
|
'com.jinzhi.butler/web_file_picker',
|
||||||
);
|
);
|
||||||
|
|
||||||
late final DWebViewController _controller;
|
late final DWebViewController _controller;
|
||||||
|
|||||||
+1
-1
@@ -591,7 +591,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.3"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ dependencies:
|
|||||||
shadcn_ui: ^0.53.6
|
shadcn_ui: ^0.53.6
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
flutter_remix: ^0.0.3
|
flutter_remix: ^0.0.3
|
||||||
|
flutter_svg: ^2.2.3
|
||||||
not_static_icons: ^0.46.0
|
not_static_icons: ^0.46.0
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
# 网络图片与磁盘缓存
|
# 网络图片与磁盘缓存
|
||||||
@@ -112,3 +113,4 @@ flutter:
|
|||||||
- shorebird.yaml
|
- shorebird.yaml
|
||||||
- assets/env/.env
|
- assets/env/.env
|
||||||
- assets/launch/
|
- assets/launch/
|
||||||
|
- assets/navigation/
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/mall/application/mall_entry_promo_store.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('records each first-install mall promotion exposure once', () async {
|
||||||
|
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||||
|
final store = MallEntryPromoStore(await SharedPreferences.getInstance());
|
||||||
|
|
||||||
|
expect(store.hasOpenedMall, isFalse);
|
||||||
|
expect(store.hasSeenHomeBadge, isFalse);
|
||||||
|
|
||||||
|
await store.markMallOpened();
|
||||||
|
await store.markHomeBadgeSeen();
|
||||||
|
|
||||||
|
expect(store.hasOpenedMall, isTrue);
|
||||||
|
expect(store.hasSeenHomeBadge, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
@@ -41,4 +43,69 @@ void main() {
|
|||||||
expect(nativeApp.opensInApp, isFalse);
|
expect(nativeApp.opensInApp, isFalse);
|
||||||
expect(external.opensInApp, isFalse);
|
expect(external.opensInApp, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows the first-install promo only for a configured mall entry', () {
|
||||||
|
const configured = MallTabConfig(
|
||||||
|
enabled: true,
|
||||||
|
url: 'https://mall.example.com',
|
||||||
|
entryPromoEnabled: true,
|
||||||
|
entryBadgeText: '新人礼',
|
||||||
|
entryTipTitle: '新人礼:¥360 铂金订购券',
|
||||||
|
entryTipBody: '点击这里进商城',
|
||||||
|
);
|
||||||
|
const disabled = MallTabConfig(
|
||||||
|
enabled: true,
|
||||||
|
url: 'https://mall.example.com',
|
||||||
|
entryPromoEnabled: false,
|
||||||
|
entryTipTitle: '新人礼',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(configured.showsEntryBadge, isTrue);
|
||||||
|
expect(configured.showsEntryTip, isTrue);
|
||||||
|
expect(disabled.showsEntryBadge, isFalse);
|
||||||
|
expect(disabled.showsEntryTip, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses the server-provided mall entry promotion fields', () {
|
||||||
|
final creative = <int>[
|
||||||
|
..._stringField(9, 'https://mall.example.com'),
|
||||||
|
..._boolField(16, true),
|
||||||
|
..._stringField(17, '新人礼'),
|
||||||
|
..._stringField(18, '新人礼:¥360 铂金订购券'),
|
||||||
|
..._stringField(19, '点击这里进商城'),
|
||||||
|
];
|
||||||
|
final config = parseMallTabConfigResponse(<int>[
|
||||||
|
..._boolField(1, true),
|
||||||
|
..._bytesField(2, creative),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(config.showsEntryBadge, isTrue);
|
||||||
|
expect(config.entryBadgeText, '新人礼');
|
||||||
|
expect(config.entryTipTitle, '新人礼:¥360 铂金订购券');
|
||||||
|
expect(config.entryTipBody, '点击这里进商城');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _stringField(int tag, String value) =>
|
||||||
|
_bytesField(tag, utf8.encode(value));
|
||||||
|
|
||||||
|
List<int> _bytesField(int tag, List<int> value) => <int>[
|
||||||
|
..._varint((tag << 3) | 2),
|
||||||
|
..._varint(value.length),
|
||||||
|
...value,
|
||||||
|
];
|
||||||
|
|
||||||
|
List<int> _boolField(int tag, bool value) => <int>[
|
||||||
|
..._varint(tag << 3),
|
||||||
|
value ? 1 : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
List<int> _varint(int value) {
|
||||||
|
final bytes = <int>[];
|
||||||
|
while (value > 127) {
|
||||||
|
bytes.add((value & 127) | 128);
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
bytes.add(value);
|
||||||
|
return bytes;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user