Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
117501130d | ||
|
|
365fd2374e | ||
|
|
1eb7c3cdbb | ||
|
|
4e730dbba8 | ||
|
|
a3e9dfee66 | ||
|
|
a1819348d5 | ||
|
|
d364c6d908 |
No files matched your search
@@ -4,8 +4,6 @@ analyzer:
|
|||||||
- "**/*.g.dart"
|
- "**/*.g.dart"
|
||||||
- "lib/models/pb/**"
|
- "lib/models/pb/**"
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
plugins:
|
|
||||||
- custom_lint
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
prefer_const_constructors: true
|
prefer_const_constructors: true
|
||||||
@@ -2,8 +2,7 @@ import java.util.Properties
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
// The Flutter Gradle Plugin must be applied after the Android plugin.
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +33,8 @@ fun requireSigningProperty(name: String): String {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.jinzhi.butler"
|
namespace = "com.jinzhi.butler"
|
||||||
compileSdk = flutter.compileSdkVersion
|
// permission_handler and the current Android plugin set require API 37.
|
||||||
|
compileSdk = 37
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -47,12 +47,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -105,6 +101,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
@@ -13,7 +14,8 @@
|
|||||||
android:name=".JinzhiApplication"
|
android:name=".JinzhiApplication"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:usesCleartextTraffic="false">
|
android:usesCleartextTraffic="false"
|
||||||
|
tools:replace="android:usesCleartextTraffic">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import com.android.build.api.dsl.LibraryExtension
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -5,7 +7,7 @@ allprojects {
|
|||||||
maven("https://maven.aliyun.com/repository/public")
|
maven("https://maven.aliyun.com/repository/public")
|
||||||
maven("https://maven.aliyun.com/repository/gradle-plugin")
|
maven("https://maven.aliyun.com/repository/gradle-plugin")
|
||||||
maven("https://maven.aliyun.com/repository/google")
|
maven("https://maven.aliyun.com/repository/google")
|
||||||
maven("https://maven.aliyun.com/repository/jcenter")
|
maven("https://developer.huawei.com/repo/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +25,24 @@ subprojects {
|
|||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Partial shrinking is needed for the obfuscated one-key-login SDK. UMeng Push
|
||||||
|
// also runs an intermediate library shrinker with minSdk 14, which is not
|
||||||
|
// supported by partial shrinking. Let the app module remain the sole release
|
||||||
|
// shrinker (its minSdk is 24) and skip the redundant library minification.
|
||||||
|
subprojects {
|
||||||
|
if (name == "umeng_push_sdk") {
|
||||||
|
afterEvaluate {
|
||||||
|
extensions.configure<LibraryExtension> {
|
||||||
|
buildTypes {
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The Gitea U-Verify plugin still targets an older Android Gradle Plugin and
|
// The Gitea U-Verify plugin still targets an older Android Gradle Plugin and
|
||||||
// declares only the manifest package. AGP 8 requires an explicit namespace;
|
// declares only the manifest package. AGP 8 requires an explicit namespace;
|
||||||
// keep the dependency as a Git package and supply its published namespace here.
|
// keep the dependency as a Git package and supply its published namespace here.
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
# AGP 9.0.1 predates API 37 but builds against it correctly; suppress its
|
||||||
|
# informational compatibility warning until Flutter ships a newer AGP template.
|
||||||
|
android.suppressUnsupportedCompileSdk=37,37.0
|
||||||
|
# U-Verify ships obfuscated verification bytecode that AGP 9/R8 cannot fully
|
||||||
|
# analyse. Keep app shrinking enabled, but route this vendor SDK family through
|
||||||
|
# D8 just as the verified reference project does.
|
||||||
|
systemProp.com.android.tools.r8.experimentalPartialShrinkingIncludePatterns=**
|
||||||
|
systemProp.com.android.tools.r8.experimentalPartialShrinkingExcludePatterns=com.mobile.auth.**,com.nirvana.**,com.cmic.**,com.umeng.umverify.**,com.unicom.**,com.a.a.**
|
||||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||||
@@ -19,8 +19,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.9.1" apply false
|
id("com.android.application") version "9.0.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>13.0</string>
|
<string>15.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,35 +1,5 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- app_tracking_transparency (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- device_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_app_update (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_native_splash (2.4.3):
|
|
||||||
- Flutter
|
|
||||||
- flutter_paid (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage_darwin (10.0.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- fluttertoast (0.0.2):
|
|
||||||
- Flutter
|
|
||||||
- package_info_plus (0.4.5):
|
|
||||||
- Flutter
|
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- permission_handler_apple (9.4.8):
|
|
||||||
- Flutter
|
|
||||||
- platform_metadata (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- sqflite_darwin (0.0.4):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- UMCCommonLog (2.0.2)
|
- UMCCommonLog (2.0.2)
|
||||||
- UMCommon (7.6.4):
|
- UMCommon (7.6.4):
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@@ -51,36 +21,12 @@ PODS:
|
|||||||
- UMCommon
|
- UMCommon
|
||||||
- UMVerify (4.0.0):
|
- UMVerify (4.0.0):
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- url_launcher_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- video_player_avfoundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`)
|
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
|
||||||
- flutter_paid (from `.symlinks/plugins/flutter_paid/ios`)
|
|
||||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
|
||||||
- platform_metadata (from `.symlinks/plugins/platform_metadata/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
|
||||||
- umeng_common_sdk (from `.symlinks/plugins/umeng_common_sdk/ios`)
|
- umeng_common_sdk (from `.symlinks/plugins/umeng_common_sdk/ios`)
|
||||||
- umeng_push_sdk (from `.symlinks/plugins/umeng_push_sdk/ios`)
|
- umeng_push_sdk (from `.symlinks/plugins/umeng_push_sdk/ios`)
|
||||||
- umeng_verify_sdk (from `.symlinks/plugins/umeng_verify_sdk/ios`)
|
- umeng_verify_sdk (from `.symlinks/plugins/umeng_verify_sdk/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -91,62 +37,17 @@ SPEC REPOS:
|
|||||||
- UMVerify
|
- UMVerify
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_tracking_transparency:
|
|
||||||
:path: ".symlinks/plugins/app_tracking_transparency/ios"
|
|
||||||
device_info_plus:
|
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_app_update:
|
|
||||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
|
||||||
flutter_native_splash:
|
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
|
||||||
flutter_paid:
|
|
||||||
:path: ".symlinks/plugins/flutter_paid/ios"
|
|
||||||
flutter_secure_storage_darwin:
|
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
|
||||||
fluttertoast:
|
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
|
||||||
package_info_plus:
|
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
permission_handler_apple:
|
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
|
||||||
platform_metadata:
|
|
||||||
:path: ".symlinks/plugins/platform_metadata/ios"
|
|
||||||
shared_preferences_foundation:
|
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
|
||||||
sqflite_darwin:
|
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
|
||||||
umeng_common_sdk:
|
umeng_common_sdk:
|
||||||
:path: ".symlinks/plugins/umeng_common_sdk/ios"
|
:path: ".symlinks/plugins/umeng_common_sdk/ios"
|
||||||
umeng_push_sdk:
|
umeng_push_sdk:
|
||||||
:path: ".symlinks/plugins/umeng_push_sdk/ios"
|
:path: ".symlinks/plugins/umeng_push_sdk/ios"
|
||||||
umeng_verify_sdk:
|
umeng_verify_sdk:
|
||||||
:path: ".symlinks/plugins/umeng_verify_sdk/ios"
|
:path: ".symlinks/plugins/umeng_verify_sdk/ios"
|
||||||
url_launcher_ios:
|
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
|
||||||
video_player_avfoundation:
|
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
|
||||||
webview_flutter_wkwebview:
|
|
||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92
|
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
|
||||||
flutter_paid: 6055d15f439071fefb849e3cebc279915ed70db5
|
|
||||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
|
||||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
|
|
||||||
platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
|
||||||
UMCCommonLog: bea707e50c85cef4b0eb47cc5c7226bb843245ca
|
UMCCommonLog: bea707e50c85cef4b0eb47cc5c7226bb843245ca
|
||||||
UMCommon: 001691e59cb7961ec21d3312687499c8516dddc6
|
UMCommon: 001691e59cb7961ec21d3312687499c8516dddc6
|
||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
@@ -155,9 +56,6 @@ SPEC CHECKSUMS:
|
|||||||
umeng_verify_sdk: 64a823bd4528bf46c40759c613d6ab34935b8293
|
umeng_verify_sdk: 64a823bd4528bf46c40759c613d6ab34935b8293
|
||||||
UMPush: e9e81f259799b2b238ee21cc152e4cea31909604
|
UMPush: e9e81f259799b2b238ee21cc152e4cea31909604
|
||||||
UMVerify: d961ffe2e4b9f00ad578c373e420ad17f72a1f7c
|
UMVerify: d961ffe2e4b9f00ad578c373e420ad17f72a1f7c
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
|
||||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
|
||||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: c5e833ba06eec2784e07140760ce1599fc8e3bfc
|
PODFILE CHECKSUM: c5e833ba06eec2784e07140760ce1599fc8e3bfc
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C52A653951E0B42558DD134A /* Pods_Runner.framework */; };
|
7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C52A653951E0B42558DD134A /* Pods_Runner.framework */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
738C53128F0E1E92C051CFBA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
738C53128F0E1E92C051CFBA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
8EB9610D6597FC7A482D1606 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
8EB9610D6597FC7A482D1606 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -80,6 +84,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */,
|
7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -107,6 +112,7 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -146,6 +152,7 @@
|
|||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
@@ -197,7 +204,6 @@
|
|||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */,
|
|
||||||
90B1F719ECC32B8C9ADF27D1 /* [CP] Copy Pods Resources */,
|
90B1F719ECC32B8C9ADF27D1 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
@@ -205,6 +211,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@@ -238,6 +247,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@@ -292,27 +304,6 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -359,14 +350,10 @@
|
|||||||
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";
|
||||||
@@ -403,6 +390,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -480,7 +468,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -614,7 +602,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -665,7 +653,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -763,6 +751,20 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,24 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import Flutter
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,27 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
@@ -15,6 +17,10 @@ import '../common/theme/theme_controller.dart';
|
|||||||
import '../features/ads/application/ad_config_foreground_refresher.dart';
|
import '../features/ads/application/ad_config_foreground_refresher.dart';
|
||||||
import '../features/settings/application/update_foreground_service.dart';
|
import '../features/settings/application/update_foreground_service.dart';
|
||||||
import '../features/web/application/mall_web_host_controller.dart';
|
import '../features/web/application/mall_web_host_controller.dart';
|
||||||
|
import '../features/auth/application/auth_controller.dart';
|
||||||
|
import '../features/auth/application/auth_session_coordinator.dart';
|
||||||
|
import '../features/auth/data/auth_models.dart';
|
||||||
|
import '../features/assets/application/asset_portfolio_controller.dart';
|
||||||
import '../common/router/deep_link_service.dart';
|
import '../common/router/deep_link_service.dart';
|
||||||
import '../common/services/device_header_service.dart';
|
import '../common/services/device_header_service.dart';
|
||||||
|
|
||||||
@@ -28,14 +34,43 @@ class JinzhiApp extends ConsumerStatefulWidget {
|
|||||||
class _JinzhiAppState extends ConsumerState<JinzhiApp>
|
class _JinzhiAppState extends ConsumerState<JinzhiApp>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
late final DevicePushRegistrar _pushRegistrar;
|
late final DevicePushRegistrar _pushRegistrar;
|
||||||
|
ProviderSubscription<AsyncValue<AuthState>>? _authSubscription;
|
||||||
|
StreamSubscription<Uri>? _appLinkSubscription;
|
||||||
|
late final SessionEndListener _sessionEndListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_pushRegistrar = DevicePushRegistrar(ref.read)..start();
|
_pushRegistrar = DevicePushRegistrar(ref.read)..start();
|
||||||
|
_sessionEndListener = (reason) {
|
||||||
|
if (reason == SessionEndReason.accountDeleted) {
|
||||||
|
ref.read(assetPortfolioControllerProvider.notifier).clearHoldings();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ref.read(authSessionCoordinatorProvider).addListener(_sessionEndListener);
|
||||||
|
_authSubscription = ref.listenManual<AsyncValue<AuthState>>(authProvider, (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) {
|
||||||
|
final oldUser = previous?.value is LoggedInAuthState
|
||||||
|
? (previous!.value! as LoggedInAuthState).user.id
|
||||||
|
: null;
|
||||||
|
final newUser = next.value is LoggedInAuthState
|
||||||
|
? (next.value! as LoggedInAuthState).user.id
|
||||||
|
: null;
|
||||||
|
if (oldUser != newUser) _pushRegistrar.forceRegisterCurrentToken();
|
||||||
|
DeepLinkService.notifyAuthChanged();
|
||||||
|
});
|
||||||
UmengPushService.configureNotificationOpen(DeepLinkService.handle);
|
UmengPushService.configureNotificationOpen(DeepLinkService.handle);
|
||||||
|
unawaited(_startAppLinks());
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
final context = rootNavigatorKey.currentContext;
|
||||||
|
if (context == null) return false;
|
||||||
|
context.go(location);
|
||||||
|
return true;
|
||||||
|
}, authGuard: (_) => !ref.read(authProvider).isLoading);
|
||||||
// 首次 iOS 启动在欢迎页点击进入后才注册 APNs,确保系统弹窗不被启动页
|
// 首次 iOS 启动在欢迎页点击进入后才注册 APNs,确保系统弹窗不被启动页
|
||||||
// 或其他 Overlay 吞掉;后续启动和 Android 保持原有自动初始化。
|
// 或其他 Overlay 吞掉;后续启动和 Android 保持原有自动初始化。
|
||||||
if (!DeviceHeaderService.to.iosStartupRequired) {
|
if (!DeviceHeaderService.to.iosStartupRequired) {
|
||||||
@@ -44,9 +79,29 @@ class _JinzhiAppState extends ConsumerState<JinzhiApp>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _startAppLinks() async {
|
||||||
|
try {
|
||||||
|
final appLinks = AppLinks();
|
||||||
|
final initial = await appLinks.getInitialLink();
|
||||||
|
if (initial != null) DeepLinkService.handleUri(initial);
|
||||||
|
_appLinkSubscription = appLinks.uriLinkStream.listen(
|
||||||
|
DeepLinkService.handleUri,
|
||||||
|
onError: (_) {},
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Desktop tests and unsupported platforms may not register the plugin.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
unawaited(_pushRegistrar.stop());
|
unawaited(_pushRegistrar.stop());
|
||||||
|
_authSubscription?.close();
|
||||||
|
ref
|
||||||
|
.read(authSessionCoordinatorProvider)
|
||||||
|
.removeListener(_sessionEndListener);
|
||||||
|
unawaited(_appLinkSubscription?.cancel());
|
||||||
|
DeepLinkService.detachRouter();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart';
|
|||||||
import '../common/services/device_header_service.dart';
|
import '../common/services/device_header_service.dart';
|
||||||
import '../common/config/shorebird_service.dart';
|
import '../common/config/shorebird_service.dart';
|
||||||
import '../common/config/umeng_config.dart';
|
import '../common/config/umeng_config.dart';
|
||||||
import '../common/storage/secure_identity_storage.dart';
|
|
||||||
import '../common/storage/storage_service.dart';
|
import '../common/storage/storage_service.dart';
|
||||||
import '../common/widgets/easy_refresh_localization.dart';
|
import '../common/widgets/easy_refresh_localization.dart';
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
@@ -17,9 +16,6 @@ Future<Widget> bootstrap() async {
|
|||||||
configureEasyRefreshChineseLocalization();
|
configureEasyRefreshChineseLocalization();
|
||||||
await UmengConfig.load();
|
await UmengConfig.load();
|
||||||
await StorageService.instance;
|
await StorageService.instance;
|
||||||
await SecureIdentityStorage(
|
|
||||||
StorageService.to.preferences,
|
|
||||||
).restoreAccessToken();
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// Before consent this only initializes app version and channel. The
|
// Before consent this only initializes app version and channel. The
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import 'api_exception.dart';
|
||||||
|
|
||||||
String? kratosReason(Object e) {
|
String? kratosReason(Object e) {
|
||||||
if (e is! DioException) return null;
|
if (e is DioException || e is ApiException || e is FormatException) {
|
||||||
final body = e.response?.data;
|
return ApiException.from(e).reason;
|
||||||
if (body is Map && body['reason'] is String) return body['reason'] as String;
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String kratosDisplayMessage(Object e, {required String fallback}) {
|
String kratosDisplayMessage(Object e, {required String fallback}) {
|
||||||
if (kratosReason(e) == 'INTERNAL_ERROR') return fallback;
|
if (kratosReason(e) == 'INTERNAL_ERROR') return fallback;
|
||||||
if (e is DioException) {
|
final exception = ApiException.from(e);
|
||||||
final body = e.response?.data;
|
if (exception.serverMessage case final message? when message.isNotEmpty) {
|
||||||
if (body is Map && body['message'] is String) {
|
return message;
|
||||||
final m = body['message'] as String;
|
|
||||||
if (m.isNotEmpty) return m;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract final class ErrorMessageMapper {
|
||||||
|
static String message(Object error, {required String fallback}) =>
|
||||||
|
kratosDisplayMessage(error, fallback: fallback);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import '../../models/pb/errors/errors.pb.dart' as kerrors;
|
||||||
|
|
||||||
|
enum ApiExceptionKind { transport, http, backend, decode, cancelled }
|
||||||
|
|
||||||
|
/// The single network failure shape exposed to application code.
|
||||||
|
class ApiException implements Exception {
|
||||||
|
const ApiException({
|
||||||
|
required this.kind,
|
||||||
|
this.httpStatus,
|
||||||
|
this.backendCode,
|
||||||
|
this.reason,
|
||||||
|
this.serverMessage,
|
||||||
|
this.requestOptions,
|
||||||
|
this.cause,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ApiExceptionKind kind;
|
||||||
|
final int? httpStatus;
|
||||||
|
final int? backendCode;
|
||||||
|
final String? reason;
|
||||||
|
final String? serverMessage;
|
||||||
|
final RequestOptions? requestOptions;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
bool get isUnauthorized =>
|
||||||
|
httpStatus == 401 ||
|
||||||
|
const {'INVALID_TOKEN', 'TOKEN_EXPIRED'}.contains(reason?.toUpperCase());
|
||||||
|
|
||||||
|
bool get isRetryable {
|
||||||
|
if (kind == ApiExceptionKind.transport) return true;
|
||||||
|
final status = httpStatus;
|
||||||
|
return status == 408 || status == 429 || (status != null && status >= 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get sessionGeneration =>
|
||||||
|
requestOptions?.extra[authSessionGenerationExtra] as int?;
|
||||||
|
|
||||||
|
static ApiException from(Object error, {StackTrace? stackTrace}) {
|
||||||
|
if (error is ApiException) return error;
|
||||||
|
if (error is DioException) {
|
||||||
|
if (error.error is ApiException) return error.error! as ApiException;
|
||||||
|
return fromDio(error);
|
||||||
|
}
|
||||||
|
if (error is FormatException) {
|
||||||
|
return ApiException(kind: ApiExceptionKind.decode, cause: error);
|
||||||
|
}
|
||||||
|
return ApiException(kind: ApiExceptionKind.decode, cause: error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ApiException fromDio(DioException error) {
|
||||||
|
final response = error.response;
|
||||||
|
final status = response?.statusCode;
|
||||||
|
final kratos = parseKratosError(response?.data);
|
||||||
|
if (error.type == DioExceptionType.cancel) {
|
||||||
|
return ApiException(
|
||||||
|
kind: ApiExceptionKind.cancelled,
|
||||||
|
httpStatus: status,
|
||||||
|
requestOptions: error.requestOptions,
|
||||||
|
cause: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final kind = kratos != null
|
||||||
|
? ApiExceptionKind.backend
|
||||||
|
: status != null
|
||||||
|
? ApiExceptionKind.http
|
||||||
|
: ApiExceptionKind.transport;
|
||||||
|
return ApiException(
|
||||||
|
kind: kind,
|
||||||
|
httpStatus: status,
|
||||||
|
backendCode: kratos?.code,
|
||||||
|
reason: _normalized(kratos?.reason),
|
||||||
|
serverMessage: _normalized(kratos?.message),
|
||||||
|
requestOptions: error.requestOptions,
|
||||||
|
cause: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ApiException($kind, status: $httpStatus, code: $backendCode, '
|
||||||
|
'reason: $reason, message: $serverMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const authSessionGenerationExtra = 'authSessionGeneration';
|
||||||
|
|
||||||
|
kerrors.Status? parseKratosError(Object? raw) {
|
||||||
|
if (raw is List<int> && raw.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final status = kerrors.Status.fromBuffer(raw);
|
||||||
|
if (status.code != 0 || status.reason.isNotEmpty) return status;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw is Map) {
|
||||||
|
final code = raw['code'];
|
||||||
|
final reason = '${raw['reason'] ?? ''}';
|
||||||
|
final message = '${raw['message'] ?? ''}';
|
||||||
|
if (code is num || reason.isNotEmpty || message.isNotEmpty) {
|
||||||
|
return kerrors.Status(
|
||||||
|
code: code is num ? code.toInt() : 0,
|
||||||
|
reason: reason,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _normalized(String? value) {
|
||||||
|
final result = value?.trim() ?? '';
|
||||||
|
return result.isEmpty ? null : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiExceptionInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
final apiException = ApiException.fromDio(err);
|
||||||
|
handler.next(err.copyWith(error: apiException));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import 'package:dio/dio.dart';
|
|||||||
|
|
||||||
import '../services/device_header_service.dart';
|
import '../services/device_header_service.dart';
|
||||||
import '../storage/secure_identity_storage.dart';
|
import '../storage/secure_identity_storage.dart';
|
||||||
|
import '../storage/preferences_storage.dart';
|
||||||
import '../storage/storage_service.dart';
|
import '../storage/storage_service.dart';
|
||||||
|
import 'api_exception.dart';
|
||||||
|
|
||||||
final _defaultIdentityStorage = SecureIdentityStorage(
|
final _defaultIdentityStorage = SecureIdentityStorage(
|
||||||
StorageService.to.preferences,
|
PreferencesStorage(StorageService.to.preferences),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> saveJwt(String token) => _defaultIdentityStorage.saveJwt(token);
|
Future<void> saveJwt(String token) => _defaultIdentityStorage.saveJwt(token);
|
||||||
@@ -13,22 +15,51 @@ Future<void> clearJwt() => _defaultIdentityStorage.clearJwt();
|
|||||||
Future<String?> loadJwt() => _defaultIdentityStorage.readJwt();
|
Future<String?> loadJwt() => _defaultIdentityStorage.readJwt();
|
||||||
|
|
||||||
class AuthInterceptor extends Interceptor {
|
class AuthInterceptor extends Interceptor {
|
||||||
AuthInterceptor(this._identityStorage);
|
AuthInterceptor(this._sessionStorage, {required Uri backendOrigin})
|
||||||
|
: _backendOrigin = backendOrigin;
|
||||||
|
|
||||||
final SecureIdentityStorage _identityStorage;
|
final AuthSessionStorage _sessionStorage;
|
||||||
|
final Uri _backendOrigin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onRequest(
|
Future<void> onRequest(
|
||||||
RequestOptions options,
|
RequestOptions options,
|
||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
|
if (!_sameOrigin(options.uri, _backendOrigin)) {
|
||||||
|
_stripSensitiveHeaders(options.headers);
|
||||||
|
handler.next(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await DeviceHeaderService.to.ensureInitialized();
|
await DeviceHeaderService.to.ensureInitialized();
|
||||||
options.headers.addAll(DeviceHeaderService.to.commonHeaders());
|
options.headers.addAll(DeviceHeaderService.to.commonHeaders());
|
||||||
options.headers.addAll(DeviceHeaderService.to.platformIdentityHeaders());
|
options.headers.addAll(DeviceHeaderService.to.platformIdentityHeaders());
|
||||||
final jwt = await _identityStorage.readJwt();
|
final jwt = _sessionStorage.accessToken ?? await _sessionStorage.restore();
|
||||||
if (jwt != null && jwt.isNotEmpty) {
|
if (jwt != null && jwt.isNotEmpty) {
|
||||||
options.headers['Authorization'] = 'Bearer $jwt';
|
options.headers['Authorization'] = 'Bearer $jwt';
|
||||||
}
|
}
|
||||||
|
options.extra[authSessionGenerationExtra] = _sessionStorage.generation;
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _sameOrigin(Uri request, Uri backend) =>
|
||||||
|
request.scheme.toLowerCase() == backend.scheme.toLowerCase() &&
|
||||||
|
request.host.toLowerCase() == backend.host.toLowerCase() &&
|
||||||
|
request.port == backend.port;
|
||||||
|
|
||||||
|
void _stripSensitiveHeaders(Map<String, dynamic> headers) {
|
||||||
|
const sensitive = {
|
||||||
|
'authorization',
|
||||||
|
'x-device-id',
|
||||||
|
'user-devicetoken',
|
||||||
|
'user-android-id',
|
||||||
|
'user-android-oaid',
|
||||||
|
'user-android-imei',
|
||||||
|
'user-android-mac',
|
||||||
|
'user-ios-idfv',
|
||||||
|
'user-ios-idfa',
|
||||||
|
'user-ios-paid',
|
||||||
|
};
|
||||||
|
headers.removeWhere((key, _) => sensitive.contains(key.toLowerCase()));
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
|
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
import '../storage/secure_identity_storage.dart';
|
import '../storage/secure_identity_storage.dart';
|
||||||
|
import '../../features/auth/application/auth_session_coordinator.dart';
|
||||||
|
import 'api_exception.dart';
|
||||||
import 'auth_interceptor.dart';
|
import 'auth_interceptor.dart';
|
||||||
import 'kratos_error_interceptor.dart';
|
import 'kratos_error_interceptor.dart';
|
||||||
|
|
||||||
@@ -26,7 +28,9 @@ AppConfig appConfig(Ref ref) => AppConfig.fromEnvironment();
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Dio dio(Ref ref) {
|
Dio dio(Ref ref) {
|
||||||
final config = ref.watch(appConfigProvider);
|
final config = ref.watch(appConfigProvider);
|
||||||
final identityStorage = ref.watch(secureIdentityStorageProvider);
|
final sessionStorage = ref.watch(authSessionStorageProvider);
|
||||||
|
final coordinator = ref.watch(authSessionCoordinatorProvider);
|
||||||
|
final backendOrigin = Uri.parse(config.apiBaseUrl);
|
||||||
final d = Dio(
|
final d = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
baseUrl: config.apiBaseUrl,
|
baseUrl: config.apiBaseUrl,
|
||||||
@@ -40,11 +44,57 @@ Dio dio(Ref ref) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
d.interceptors.add(AuthInterceptor(identityStorage));
|
d.interceptors.add(
|
||||||
|
AuthInterceptor(sessionStorage, backendOrigin: backendOrigin),
|
||||||
|
);
|
||||||
d.interceptors.add(KratosErrorInterceptor());
|
d.interceptors.add(KratosErrorInterceptor());
|
||||||
|
d.interceptors.add(_SessionExpiryInterceptor(coordinator));
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
d.interceptors.add(LogInterceptor(requestBody: false, responseBody: false));
|
d.interceptors.add(LogInterceptor(requestBody: false, responseBody: false));
|
||||||
|
d.interceptors.add(FancyDioInterceptor());
|
||||||
}
|
}
|
||||||
d.interceptors.add(FancyDioInterceptor());
|
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explicit name for new code. [dioProvider] remains as a compatibility alias
|
||||||
|
/// while existing feature clients are migrated incrementally.
|
||||||
|
final backendDioProvider = dioProvider;
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Dio publicDio(Ref ref) => Dio(
|
||||||
|
BaseOptions(
|
||||||
|
connectTimeout: const Duration(seconds: 15),
|
||||||
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class _SessionExpiryInterceptor extends Interceptor {
|
||||||
|
_SessionExpiryInterceptor(this._coordinator);
|
||||||
|
|
||||||
|
final AuthSessionCoordinator _coordinator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onError(
|
||||||
|
DioException err,
|
||||||
|
ErrorInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
final exception = ApiException.from(err);
|
||||||
|
if (exception.isUnauthorized) {
|
||||||
|
// Network code only emits the session event. Navigation and presentation
|
||||||
|
// observe AuthState elsewhere.
|
||||||
|
try {
|
||||||
|
await _coordinator.expireIfNeeded(exception);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
FlutterError.reportError(
|
||||||
|
FlutterErrorDetails(
|
||||||
|
exception: error,
|
||||||
|
stack: stackTrace,
|
||||||
|
library: 'jinzhi network',
|
||||||
|
context: ErrorDescription('while expiring an unauthorized session'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,16 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
import '../../models/pb/errors/errors.pb.dart' as kerrors;
|
import 'api_exception.dart';
|
||||||
|
|
||||||
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;
|
if (err.error is ApiException) {
|
||||||
final raw = response?.data;
|
handler.next(err);
|
||||||
if (response != null && raw is List<int> && raw.isNotEmpty) {
|
return;
|
||||||
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);
|
// Preserve response.data for diagnostics and retries. The parsed error is
|
||||||
|
// carried as DioException.error and normalized by ApiException.from().
|
||||||
|
handler.next(err.copyWith(error: ApiException.fromDio(err)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// Minimal reader retained only for endpoints that do not have an
|
||||||
|
/// authoritative generated protobuf model yet.
|
||||||
|
class ProtobufWireMessage {
|
||||||
|
ProtobufWireMessage(this.bytes);
|
||||||
|
|
||||||
|
final List<int> bytes;
|
||||||
|
Map<int, List<Object>>? _cache;
|
||||||
|
|
||||||
|
Map<int, List<Object>> get _fields => _cache ??= _read();
|
||||||
|
int integer(int tag) => (_fields[tag]?.lastOrNull as int?) ?? 0;
|
||||||
|
|
||||||
|
String string(int tag) {
|
||||||
|
final value = _fields[tag]?.lastOrNull;
|
||||||
|
return value is List<int> ? utf8.decode(value, allowMalformed: true) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<int, List<Object>> _read() {
|
||||||
|
final result = <int, List<Object>>{};
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
int varint() {
|
||||||
|
var value = 0;
|
||||||
|
var shift = 0;
|
||||||
|
while (index < bytes.length) {
|
||||||
|
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 = varint();
|
||||||
|
final tag = key >> 3;
|
||||||
|
final Object field;
|
||||||
|
if ((key & 7) == 0) {
|
||||||
|
field = varint();
|
||||||
|
} else if ((key & 7) == 2) {
|
||||||
|
final length = varint();
|
||||||
|
final end = index + length;
|
||||||
|
if (end > bytes.length) throw const FormatException('Invalid protobuf');
|
||||||
|
field = bytes.sublist(index, end);
|
||||||
|
index = end;
|
||||||
|
} else {
|
||||||
|
throw const FormatException('Unsupported protobuf');
|
||||||
|
}
|
||||||
|
(result[tag] ??= []).add(field);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on List<Object> {
|
||||||
|
Object? get lastOrNull => isEmpty ? null : last;
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/misc.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
import '../network/dio_provider.dart';
|
import '../../models/pb/jinzhi/v1/push.pb.dart' as pb;
|
||||||
|
import '../network/api_exception.dart';
|
||||||
import 'notification_permission.dart';
|
import 'notification_permission.dart';
|
||||||
|
import 'push_api_client.dart';
|
||||||
import 'umeng_push_service.dart';
|
import 'umeng_push_service.dart';
|
||||||
|
|
||||||
class DevicePushRegistrar {
|
class DevicePushRegistrar {
|
||||||
@@ -15,81 +15,52 @@ class DevicePushRegistrar {
|
|||||||
StreamSubscription<String>? _subscription;
|
StreamSubscription<String>? _subscription;
|
||||||
String? _lastToken;
|
String? _lastToken;
|
||||||
bool? _lastPermission;
|
bool? _lastPermission;
|
||||||
int _attempt = 0;
|
Future<void> _registrationQueue = Future.value();
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
_subscription ??= UmengPushService.onDeviceToken.listen(_register);
|
_subscription ??= UmengPushService.onDeviceToken.listen(_enqueue);
|
||||||
UmengPushService.emitCachedToken();
|
UmengPushService.emitCachedToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async => _subscription?.cancel();
|
Future<void> stop() async => _subscription?.cancel();
|
||||||
Future<void> refresh() => UmengPushService.refreshDeviceToken();
|
Future<void> refresh() => UmengPushService.refreshDeviceToken();
|
||||||
void forceRegisterCurrentToken() {
|
void forceRegisterCurrentToken() {
|
||||||
|
// BLOCKED(server contract): guest registration is intentionally preserved.
|
||||||
|
// Do not add a client-side unbind call until the backend confirms whether
|
||||||
|
// guest registration reliably clears the previous user binding.
|
||||||
_lastToken = null;
|
_lastToken = null;
|
||||||
_lastPermission = null;
|
_lastPermission = null;
|
||||||
UmengPushService.emitCachedToken();
|
UmengPushService.emitCachedToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _enqueue(String token) {
|
||||||
|
_registrationQueue = _registrationQueue
|
||||||
|
.catchError((_) {})
|
||||||
|
.then((_) => _register(token));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _register(String token) async {
|
Future<void> _register(String token) async {
|
||||||
final permission = await NotificationPermission.isGranted();
|
final permission = await NotificationPermission.isGranted();
|
||||||
if (token == _lastToken && permission == _lastPermission) return;
|
if (token == _lastToken && permission == _lastPermission) return;
|
||||||
try {
|
final package = await PackageInfo.fromPlatform();
|
||||||
final package = await PackageInfo.fromPlatform();
|
final request = pb.RegisterPushDeviceRequest(
|
||||||
await _read(dioProvider).post<List<int>>(
|
umengDeviceToken: token,
|
||||||
'/api/v1/push/devices',
|
platform: defaultTargetPlatform == TargetPlatform.iOS
|
||||||
data: _PushRegisterRequest(
|
? pb.Platform.PLATFORM_IOS
|
||||||
token: token,
|
: pb.Platform.PLATFORM_ANDROID,
|
||||||
platform: defaultTargetPlatform == TargetPlatform.iOS ? 1 : 2,
|
appVersion: package.version,
|
||||||
version: package.version,
|
permissionGranted: permission,
|
||||||
permission: permission,
|
);
|
||||||
).toBytes(),
|
for (var attempt = 0; attempt < 4; attempt++) {
|
||||||
options: protobufOptions(),
|
try {
|
||||||
);
|
await _read(pushApiClientProvider).registerDevice(request);
|
||||||
_lastToken = token;
|
_lastToken = token;
|
||||||
_lastPermission = permission;
|
_lastPermission = permission;
|
||||||
_attempt = 0;
|
return;
|
||||||
} catch (_) {
|
} catch (error) {
|
||||||
if (_attempt++ >= 3) return;
|
if (attempt == 3 || !ApiException.from(error).isRetryable) return;
|
||||||
await Future<void>.delayed(Duration(seconds: 1 << (_attempt - 1)));
|
await Future<void>.delayed(Duration(seconds: 1 << attempt));
|
||||||
await _register(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PushRegisterRequest {
|
|
||||||
const _PushRegisterRequest({
|
|
||||||
required this.token,
|
|
||||||
required this.platform,
|
|
||||||
required this.version,
|
|
||||||
required this.permission,
|
|
||||||
});
|
|
||||||
final String token;
|
|
||||||
final int platform;
|
|
||||||
final String version;
|
|
||||||
final bool permission;
|
|
||||||
Uint8List toBytes() {
|
|
||||||
final bytes = BytesBuilder(copy: false);
|
|
||||||
void putInt(int value) {
|
|
||||||
while (value > 0x7f) {
|
|
||||||
bytes.addByte((value & 0x7f) | 0x80);
|
|
||||||
value >>= 7;
|
|
||||||
}
|
}
|
||||||
bytes.addByte(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void putString(int tag, String value) {
|
|
||||||
final raw = value.codeUnits;
|
|
||||||
putInt((tag << 3) | 2);
|
|
||||||
putInt(raw.length);
|
|
||||||
bytes.add(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
putString(1, token);
|
|
||||||
putInt(2 << 3);
|
|
||||||
putInt(platform);
|
|
||||||
putString(3, version);
|
|
||||||
putInt(4 << 3);
|
|
||||||
putInt(permission ? 1 : 0);
|
|
||||||
return bytes.toBytes();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
// ignore: unused_import — compute() is referenced by generated Retrofit code.
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
|
import '../../models/pb/jinzhi/v1/push.pb.dart';
|
||||||
|
import '../network/dio_provider.dart';
|
||||||
|
|
||||||
|
part 'push_api_client.g.dart';
|
||||||
|
|
||||||
|
@RestApi()
|
||||||
|
abstract class PushApiClient {
|
||||||
|
factory PushApiClient(Dio dio, {String? baseUrl}) = _PushApiClient;
|
||||||
|
|
||||||
|
@POST('/api/v1/push/devices')
|
||||||
|
Future<RegisterPushDeviceReply> registerDevice(
|
||||||
|
@Body() RegisterPushDeviceRequest request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final pushApiClientProvider = Provider<PushApiClient>(
|
||||||
|
(ref) => PushApiClient(ref.watch(backendDioProvider)),
|
||||||
|
);
|
||||||
@@ -12,11 +12,12 @@ import '../storage/storage_keys.dart';
|
|||||||
import '../storage/storage_service.dart';
|
import '../storage/storage_service.dart';
|
||||||
|
|
||||||
abstract final class UmengPushService {
|
abstract final class UmengPushService {
|
||||||
|
static const _maxPendingNotificationOpens = 20;
|
||||||
static final _tokens = StreamController<String>.broadcast();
|
static final _tokens = StreamController<String>.broadcast();
|
||||||
static Future<void>? _starting;
|
static Future<void>? _starting;
|
||||||
static bool _started = false;
|
static bool _started = false;
|
||||||
static void Function(String payload)? _onNotificationOpen;
|
static void Function(String payload)? _onNotificationOpen;
|
||||||
static String? _pendingNotificationOpen;
|
static final List<String> _pendingNotificationOpens = [];
|
||||||
|
|
||||||
static Stream<String> get onDeviceToken => _tokens.stream;
|
static Stream<String> get onDeviceToken => _tokens.stream;
|
||||||
|
|
||||||
@@ -24,10 +25,10 @@ abstract final class UmengPushService {
|
|||||||
void Function(String payload) callback,
|
void Function(String payload) callback,
|
||||||
) {
|
) {
|
||||||
_onNotificationOpen = callback;
|
_onNotificationOpen = callback;
|
||||||
final pending = _pendingNotificationOpen;
|
final pending = List<String>.of(_pendingNotificationOpens);
|
||||||
if (pending != null) {
|
_pendingNotificationOpens.clear();
|
||||||
_pendingNotificationOpen = null;
|
for (final payload in pending) {
|
||||||
callback(pending);
|
callback(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,10 @@ abstract final class UmengPushService {
|
|||||||
UmengPushSdk.setNotificationCallback((_) {}, (payload) {
|
UmengPushSdk.setNotificationCallback((_) {}, (payload) {
|
||||||
final callback = _onNotificationOpen;
|
final callback = _onNotificationOpen;
|
||||||
if (callback == null) {
|
if (callback == null) {
|
||||||
_pendingNotificationOpen = payload;
|
if (_pendingNotificationOpens.length == _maxPendingNotificationOpens) {
|
||||||
|
_pendingNotificationOpens.removeAt(0);
|
||||||
|
}
|
||||||
|
_pendingNotificationOpens.add(payload);
|
||||||
} else {
|
} else {
|
||||||
callback(payload);
|
callback(payload);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,177 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:go_router/go_router.dart';
|
enum DeepLinkSource { push, appLink, universalLink, customScheme }
|
||||||
|
|
||||||
import 'app_router.dart';
|
typedef DeepLinkNavigator = bool Function(String location);
|
||||||
|
typedef DeepLinkAuthGuard = bool Function(String location);
|
||||||
|
|
||||||
/// 统一将 Scheme、Universal Link 和推送 payload 收敛为应用内路由。
|
class DeepLinkEvent {
|
||||||
|
const DeepLinkEvent({
|
||||||
|
required this.raw,
|
||||||
|
required this.source,
|
||||||
|
required this.receivedAt,
|
||||||
|
this.eventId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String raw;
|
||||||
|
final DeepLinkSource source;
|
||||||
|
final DateTime receivedAt;
|
||||||
|
final String? eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source adapters feed one parser and a bounded FIFO. Navigation starts only
|
||||||
|
/// after GoRouter has a context and the auth guard accepts the target.
|
||||||
abstract final class DeepLinkService {
|
abstract final class DeepLinkService {
|
||||||
static void handle(String raw) {
|
static const maxPending = 20;
|
||||||
final target = _route(raw);
|
static const expiry = Duration(minutes: 15);
|
||||||
if (target != null) rootNavigatorKey.currentContext?.go(target);
|
static const _environmentHosts = String.fromEnvironment(
|
||||||
|
'JINZHI_DEEP_LINK_HOSTS',
|
||||||
|
);
|
||||||
|
|
||||||
|
static final List<DeepLinkEvent> _pending = [];
|
||||||
|
static final Set<String> _pendingEventIds = {};
|
||||||
|
static final Map<String, DateTime> _seenEventIds = {};
|
||||||
|
static final Set<String> _configuredHttpsHosts = _normalizedHosts(
|
||||||
|
_environmentHosts.split(','),
|
||||||
|
);
|
||||||
|
static Set<String> _trustedHttpsHosts = {..._configuredHttpsHosts};
|
||||||
|
static DeepLinkNavigator? _navigator;
|
||||||
|
static DeepLinkAuthGuard _authGuard = (_) => true;
|
||||||
|
|
||||||
|
static int get pendingCount => _pending.length;
|
||||||
|
|
||||||
|
static void handle(String raw) => enqueue(raw, source: DeepLinkSource.push);
|
||||||
|
|
||||||
|
static void handleUri(Uri uri) => enqueue(
|
||||||
|
uri.toString(),
|
||||||
|
source: uri.scheme.toLowerCase() == 'jinzhi'
|
||||||
|
? DeepLinkSource.customScheme
|
||||||
|
: DeepLinkSource.universalLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
static void enqueue(
|
||||||
|
String raw, {
|
||||||
|
required DeepLinkSource source,
|
||||||
|
DateTime? receivedAt,
|
||||||
|
}) {
|
||||||
|
final normalized = raw.trim();
|
||||||
|
if (normalized.isEmpty) return;
|
||||||
|
final event = DeepLinkEvent(
|
||||||
|
raw: normalized,
|
||||||
|
source: source,
|
||||||
|
receivedAt: receivedAt ?? DateTime.now(),
|
||||||
|
eventId: _eventId(normalized),
|
||||||
|
);
|
||||||
|
_discardExpired(event.receivedAt);
|
||||||
|
_seenEventIds.removeWhere(
|
||||||
|
(_, seenAt) => event.receivedAt.difference(seenAt) > expiry,
|
||||||
|
);
|
||||||
|
if (event.eventId case final id? when _seenEventIds.containsKey(id)) return;
|
||||||
|
if (_pending.length == maxPending) _removeAt(0);
|
||||||
|
_pending.add(event);
|
||||||
|
if (event.eventId case final id?) {
|
||||||
|
_pendingEventIds.add(id);
|
||||||
|
_seenEventIds[id] = event.receivedAt;
|
||||||
|
}
|
||||||
|
_drain();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void attachRouter(
|
||||||
|
DeepLinkNavigator navigator, {
|
||||||
|
DeepLinkAuthGuard? authGuard,
|
||||||
|
}) {
|
||||||
|
_navigator = navigator;
|
||||||
|
_authGuard = authGuard ?? (_) => true;
|
||||||
|
_drain();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void notifyAuthChanged() => _drain();
|
||||||
|
|
||||||
|
static void detachRouter() => _navigator = null;
|
||||||
|
|
||||||
|
/// Universal/App Link domains are supplied by the release environment. An
|
||||||
|
/// empty allowlist intentionally disables HTTPS deep links until the native
|
||||||
|
/// association domain is confirmed.
|
||||||
|
static void configureTrustedHttpsHosts(Iterable<String> hosts) {
|
||||||
|
_trustedHttpsHosts = _normalizedHosts(hosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void resetForTest() {
|
||||||
|
_pending.clear();
|
||||||
|
_pendingEventIds.clear();
|
||||||
|
_seenEventIds.clear();
|
||||||
|
_trustedHttpsHosts = {..._configuredHttpsHosts};
|
||||||
|
_navigator = null;
|
||||||
|
_authGuard = (_) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drain() {
|
||||||
|
final navigator = _navigator;
|
||||||
|
if (navigator == null) return;
|
||||||
|
_discardExpired(DateTime.now());
|
||||||
|
while (_pending.isNotEmpty) {
|
||||||
|
final event = _pending.first;
|
||||||
|
final target = _route(event.raw);
|
||||||
|
if (target == null) {
|
||||||
|
_removeAt(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!_authGuard(target) || !navigator(target)) return;
|
||||||
|
_removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _discardExpired(DateTime now) {
|
||||||
|
while (_pending.isNotEmpty &&
|
||||||
|
now.difference(_pending.first.receivedAt) > expiry) {
|
||||||
|
_removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _removeAt(int index) {
|
||||||
|
final removed = _pending.removeAt(index);
|
||||||
|
if (removed.eventId case final id?) _pendingEventIds.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _eventId(String raw) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is Map) {
|
||||||
|
final id = '${decoded['event_id'] ?? decoded['eventId'] ?? ''}'.trim();
|
||||||
|
if (id.isNotEmpty) return id;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
final uri = Uri.tryParse(raw);
|
||||||
|
final id = uri?.queryParameters['event_id']?.trim();
|
||||||
|
return id == null || id.isEmpty ? null : id;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? _route(String raw) {
|
static String? _route(String raw) {
|
||||||
var source = raw.trim();
|
var source = raw.trim();
|
||||||
try {
|
try {
|
||||||
final json = jsonDecode(source);
|
final decoded = jsonDecode(source);
|
||||||
if (json is Map) {
|
if (decoded is Map) {
|
||||||
source = '${json['deep_link'] ?? json['url'] ?? json['link'] ?? ''}';
|
source =
|
||||||
|
'${decoded['deep_link'] ?? decoded['url'] ?? decoded['link'] ?? ''}';
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
final uri = Uri.tryParse(source);
|
final uri = Uri.tryParse(source);
|
||||||
if (uri == null) return null;
|
if (uri == null) return null;
|
||||||
if (uri.scheme == 'jinzhi') {
|
if (uri.scheme.toLowerCase() == 'jinzhi') {
|
||||||
final path = uri.path.isEmpty ? '/assets' : uri.path;
|
final path = uri.path.isNotEmpty
|
||||||
|
? uri.path
|
||||||
|
: uri.host.isNotEmpty
|
||||||
|
? '/${uri.host}'
|
||||||
|
: '/assets';
|
||||||
return _allowed(path)
|
return _allowed(path)
|
||||||
? '$path${uri.hasQuery ? '?${uri.query}' : ''}'
|
? '$path${uri.hasQuery ? '?${uri.query}' : ''}'
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
if ((uri.scheme == 'https' || uri.scheme == 'http') && _allowed(uri.path)) {
|
if (uri.scheme.toLowerCase() == 'https' &&
|
||||||
|
uri.port == 443 &&
|
||||||
|
uri.userInfo.isEmpty &&
|
||||||
|
_trustedHttpsHosts.contains(uri.host.toLowerCase()) &&
|
||||||
|
_allowed(uri.path)) {
|
||||||
return '${uri.path}${uri.hasQuery ? '?${uri.query}' : ''}';
|
return '${uri.path}${uri.hasQuery ? '?${uri.query}' : ''}';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -40,4 +184,9 @@ abstract final class DeepLinkService {
|
|||||||
'/profile',
|
'/profile',
|
||||||
'/settings',
|
'/settings',
|
||||||
}.contains(path);
|
}.contains(path);
|
||||||
|
|
||||||
|
static Set<String> _normalizedHosts(Iterable<String> hosts) => {
|
||||||
|
for (final host in hosts)
|
||||||
|
if (host.trim().isNotEmpty) host.trim().toLowerCase(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ 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';
|
import '../storage/preferences_storage.dart';
|
||||||
|
|
||||||
class TabShell extends ConsumerStatefulWidget {
|
class TabShell extends ConsumerStatefulWidget {
|
||||||
const TabShell({
|
const TabShell({
|
||||||
@@ -65,7 +65,9 @@ class _TabShellState extends ConsumerState<TabShell>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mallEntryPromoStore = MallEntryPromoStore(StorageService.to.preferences);
|
_mallEntryPromoStore = MallEntryPromoStore(
|
||||||
|
ref.read(preferencesStorageProvider),
|
||||||
|
);
|
||||||
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
|
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
|
||||||
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
|
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
|
||||||
MallWebHostController.instance.state.addListener(_syncMallHost);
|
MallWebHostController.instance.state.addListener(_syncMallHost);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:package_info_plus/package_info_plus.dart';
|
|||||||
|
|
||||||
import '../../features/web/application/mall_web_host_controller.dart';
|
import '../../features/web/application/mall_web_host_controller.dart';
|
||||||
import '../storage/storage_service.dart';
|
import '../storage/storage_service.dart';
|
||||||
|
import '../storage/preferences_storage.dart';
|
||||||
|
|
||||||
/// Clears only disposable presentation data. Credentials and user preferences
|
/// Clears only disposable presentation data. Credentials and user preferences
|
||||||
/// deliberately remain outside this service.
|
/// deliberately remain outside this service.
|
||||||
@@ -34,7 +35,8 @@ class CacheClearService {
|
|||||||
/// in SharedPreferences and is removed here without touching user data.
|
/// in SharedPreferences and is removed here without touching user data.
|
||||||
abstract final class SplashAdControllerPlaceholder {
|
abstract final class SplashAdControllerPlaceholder {
|
||||||
static Future<void> clear() async {
|
static Future<void> clear() async {
|
||||||
await StorageService.to.preferences.remove('ads.splash.entry');
|
final preferences = PreferencesStorage(StorageService.to.preferences);
|
||||||
await StorageService.to.preferences.remove('ads.splash.daily');
|
await preferences.remove('ads.splash.entry');
|
||||||
|
await preferences.remove('ads.splash.daily');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,14 @@ import 'package:android_oaid/android_oaid.dart';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
|
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
|
||||||
import 'package:flutter_paid/flutter_paid.dart';
|
import 'package:flutter_paid/flutter_paid.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:platform_metadata/platform_metadata.dart';
|
import 'package:platform_metadata/platform_metadata.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../storage/storage_keys.dart';
|
import '../storage/storage_keys.dart';
|
||||||
import '../storage/storage_service.dart';
|
import '../storage/storage_service.dart';
|
||||||
|
import '../storage/preferences_storage.dart';
|
||||||
|
import '../storage/secure_identity_storage.dart';
|
||||||
import '../privacy/privacy_gate.dart';
|
import '../privacy/privacy_gate.dart';
|
||||||
|
|
||||||
/// 请求公共 + 平台身份 header 来源(完全对齐研值契约)。
|
/// 请求公共 + 平台身份 header 来源(完全对齐研值契约)。
|
||||||
@@ -26,8 +27,6 @@ import '../privacy/privacy_gate.dart';
|
|||||||
class DeviceHeaderService {
|
class DeviceHeaderService {
|
||||||
DeviceHeaderService._();
|
DeviceHeaderService._();
|
||||||
static final DeviceHeaderService to = DeviceHeaderService._();
|
static final DeviceHeaderService to = DeviceHeaderService._();
|
||||||
static const _storage = FlutterSecureStorage();
|
|
||||||
static const _kDeviceIdKey = 'device_id';
|
|
||||||
// static const _platformMetadataChannel = MethodChannel('platform_metadata');
|
// static const _platformMetadataChannel = MethodChannel('platform_metadata');
|
||||||
|
|
||||||
// 渠道:优先编译期 --dart-define=UMENG_CHANNEL=xxx;Android 其次读 manifest,
|
// 渠道:优先编译期 --dart-define=UMENG_CHANNEL=xxx;Android 其次读 manifest,
|
||||||
@@ -48,6 +47,9 @@ class DeviceHeaderService {
|
|||||||
Future<void>? _baseInitialization;
|
Future<void>? _baseInitialization;
|
||||||
Future<void>? _identityInitialization;
|
Future<void>? _identityInitialization;
|
||||||
|
|
||||||
|
SecureIdentityStorage get _identityStorage =>
|
||||||
|
SecureIdentityStorage(PreferencesStorage(StorageService.to.preferences));
|
||||||
|
|
||||||
Future<void> ensureInitialized() async {
|
Future<void> ensureInitialized() async {
|
||||||
_baseInitialization ??= _initializeBase();
|
_baseInitialization ??= _initializeBase();
|
||||||
await _baseInitialization;
|
await _baseInitialization;
|
||||||
@@ -89,14 +91,14 @@ class DeviceHeaderService {
|
|||||||
/// X-Device-Id:iOS 优先 IDFV(per-vendor 稳定),否则 UUIDv4;
|
/// X-Device-Id:iOS 优先 IDFV(per-vendor 稳定),否则 UUIDv4;
|
||||||
/// 首次生成后写入 secure_storage,后续复用。
|
/// 首次生成后写入 secure_storage,后续复用。
|
||||||
Future<String> _resolveDeviceId() async {
|
Future<String> _resolveDeviceId() async {
|
||||||
final stored = await _storage.read(key: _kDeviceIdKey);
|
final stored = await _identityStorage.readDeviceId();
|
||||||
if (stored != null && stored.isNotEmpty) return stored;
|
if (stored != null && stored.isNotEmpty) return stored;
|
||||||
var id = '';
|
var id = '';
|
||||||
if (!kIsWeb && Platform.isIOS) {
|
if (!kIsWeb && Platform.isIOS) {
|
||||||
id = await _resolveIdfv();
|
id = await _resolveIdfv();
|
||||||
}
|
}
|
||||||
if (id.isEmpty) id = const Uuid().v4();
|
if (id.isEmpty) id = const Uuid().v4();
|
||||||
await _storage.write(key: _kDeviceIdKey, value: id);
|
await _identityStorage.saveDeviceId(id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'storage_service.dart';
|
||||||
|
|
||||||
|
/// Typed boundary for non-sensitive preferences and display caches.
|
||||||
|
///
|
||||||
|
/// Feature code depends on this class instead of constructing or retaining a
|
||||||
|
/// [SharedPreferences] instance.
|
||||||
|
class PreferencesStorage {
|
||||||
|
const PreferencesStorage(this._preferences);
|
||||||
|
|
||||||
|
final SharedPreferences _preferences;
|
||||||
|
|
||||||
|
bool getBool(String key, {bool defaultValue = false}) =>
|
||||||
|
_preferences.getBool(key) ?? defaultValue;
|
||||||
|
String? getString(String key) => _preferences.getString(key);
|
||||||
|
bool containsKey(String key) => _preferences.containsKey(key);
|
||||||
|
|
||||||
|
Future<void> setBool(String key, bool value) =>
|
||||||
|
_preferences.setBool(key, value);
|
||||||
|
Future<void> setString(String key, String value) =>
|
||||||
|
_preferences.setString(key, value);
|
||||||
|
Future<void> remove(String key) => _preferences.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
final preferencesStorageProvider = Provider<PreferencesStorage>(
|
||||||
|
(ref) => PreferencesStorage(StorageService.to.preferences),
|
||||||
|
);
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
|
import 'preferences_storage.dart';
|
||||||
import 'storage_keys.dart';
|
import 'storage_keys.dart';
|
||||||
import 'storage_service.dart';
|
|
||||||
|
|
||||||
abstract interface class SecureKeyValueStorage {
|
abstract interface class SecureKeyValueStorage {
|
||||||
Future<String?> read(String key);
|
Future<String?> read(String key);
|
||||||
@@ -33,25 +32,33 @@ class SecureIdentityStorage {
|
|||||||
SecureKeyValueStorage secureStorage = const FlutterSecureKeyValueStorage(),
|
SecureKeyValueStorage secureStorage = const FlutterSecureKeyValueStorage(),
|
||||||
}) : _storage = secureStorage;
|
}) : _storage = secureStorage;
|
||||||
|
|
||||||
static const _accessTokenKey = 'access_token';
|
final PreferencesStorage _preferences;
|
||||||
static const _legacySecureJwtKey = 'jwt_token';
|
|
||||||
|
|
||||||
final SharedPreferences _preferences;
|
|
||||||
final SecureKeyValueStorage _storage;
|
final SecureKeyValueStorage _storage;
|
||||||
|
|
||||||
Future<String?> restoreAccessToken() async {
|
Future<String?> restoreAccessToken() async {
|
||||||
final current = _normalized(await _storage.read(_accessTokenKey));
|
|
||||||
final legacyPreferences = _normalized(
|
final legacyPreferences = _normalized(
|
||||||
_preferences.getString(storageLegacyAccessToken),
|
_preferences.getString(storageLegacyAccessToken),
|
||||||
);
|
);
|
||||||
|
final hasInstallSession = _preferences.getBool(storageAuthSessionActive);
|
||||||
|
|
||||||
|
// A plaintext token proves that this installation predates secure storage
|
||||||
|
// migration. A Keychain token by itself does not: iOS may retain it after
|
||||||
|
// uninstall while SharedPreferences (including this marker) is reset.
|
||||||
|
if (!hasInstallSession && legacyPreferences == null) {
|
||||||
|
await _deleteAllAuthCredentials();
|
||||||
|
await _preferences.remove(storageAuthSessionActive);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final current = _normalized(await _storage.read(secureAccessToken));
|
||||||
final legacySecure = current == null
|
final legacySecure = current == null
|
||||||
? _normalized(await _storage.read(_legacySecureJwtKey))
|
? _normalized(await _storage.read(secureLegacyJwtToken))
|
||||||
: null;
|
: null;
|
||||||
final token = current ?? legacyPreferences ?? legacySecure;
|
final token = current ?? legacyPreferences ?? legacySecure;
|
||||||
if (current == null && token != null) {
|
if (current == null && token != null) {
|
||||||
await _storage.write(_accessTokenKey, token);
|
await _storage.write(secureAccessToken, token);
|
||||||
}
|
}
|
||||||
await _storage.delete(_legacySecureJwtKey);
|
await _storage.delete(secureLegacyJwtToken);
|
||||||
await _preferences.remove(storageLegacyAccessToken);
|
await _preferences.remove(storageLegacyAccessToken);
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
await _preferences.remove(storageAuthSessionActive);
|
await _preferences.remove(storageAuthSessionActive);
|
||||||
@@ -62,10 +69,12 @@ class SecureIdentityStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> readJwt() async {
|
Future<String?> readJwt() async {
|
||||||
final token = _normalized(await _storage.read(_accessTokenKey));
|
if (!_preferences.getBool(storageAuthSessionActive) ||
|
||||||
if (token == null || _preferences.containsKey(storageLegacyAccessToken)) {
|
_preferences.containsKey(storageLegacyAccessToken)) {
|
||||||
return restoreAccessToken();
|
return restoreAccessToken();
|
||||||
}
|
}
|
||||||
|
final token = _normalized(await _storage.read(secureAccessToken));
|
||||||
|
if (token == null) await _preferences.remove(storageAuthSessionActive);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,22 +83,100 @@ class SecureIdentityStorage {
|
|||||||
if (normalized == null) {
|
if (normalized == null) {
|
||||||
throw ArgumentError.value(token, 'token', 'must not be empty');
|
throw ArgumentError.value(token, 'token', 'must not be empty');
|
||||||
}
|
}
|
||||||
await _storage.write(_accessTokenKey, normalized);
|
await _storage.write(secureAccessToken, normalized);
|
||||||
await _storage.delete(_legacySecureJwtKey);
|
await _storage.delete(secureLegacyJwtToken);
|
||||||
await _preferences.remove(storageLegacyAccessToken);
|
await _preferences.remove(storageLegacyAccessToken);
|
||||||
await _preferences.setBool(storageAuthSessionActive, true);
|
await _preferences.setBool(storageAuthSessionActive, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearJwt() async {
|
Future<void> clearJwt() async {
|
||||||
await _storage.delete(_accessTokenKey);
|
await _deleteAllAuthCredentials();
|
||||||
await _storage.delete(_legacySecureJwtKey);
|
|
||||||
await _preferences.remove(storageLegacyAccessToken);
|
await _preferences.remove(storageLegacyAccessToken);
|
||||||
await _preferences.remove(storageAuthSessionActive);
|
await _preferences.remove(storageAuthSessionActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> clearSessionCredentials() async {
|
||||||
|
await clearJwt();
|
||||||
|
await _storage.delete(secureMallCredential);
|
||||||
|
await _storage.delete(secureRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAccountCredentials() async {
|
||||||
|
await clearSessionCredentials();
|
||||||
|
await _storage.delete(secureBiometricCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> readDeviceId() => _storage.read(secureDeviceId);
|
||||||
|
|
||||||
|
Future<void> saveDeviceId(String deviceId) =>
|
||||||
|
_storage.write(secureDeviceId, deviceId);
|
||||||
|
|
||||||
|
Future<void> _deleteAllAuthCredentials() async {
|
||||||
|
await _storage.delete(secureAccessToken);
|
||||||
|
await _storage.delete(secureLegacyJwtToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final secureIdentityStorageProvider = Provider<SecureIdentityStorage>(
|
final secureIdentityStorageProvider = Provider<SecureIdentityStorage>(
|
||||||
(ref) => SecureIdentityStorage(StorageService.to.preferences),
|
(ref) => SecureIdentityStorage(ref.watch(preferencesStorageProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Process-memory session snapshot. Interceptors read this synchronously;
|
||||||
|
/// secure storage is touched only when a session starts, restores, or ends.
|
||||||
|
class AuthSessionStorage {
|
||||||
|
AuthSessionStorage(this._identityStorage);
|
||||||
|
|
||||||
|
final SecureIdentityStorage _identityStorage;
|
||||||
|
String? _accessToken;
|
||||||
|
int _generation = 0;
|
||||||
|
Future<String?>? _restoreFuture;
|
||||||
|
Future<void> _operationTail = Future<void>.value();
|
||||||
|
|
||||||
|
String? get accessToken => _accessToken;
|
||||||
|
int get generation => _generation;
|
||||||
|
|
||||||
|
Future<String?> restore() => _restoreFuture ??= _serialize(_restore);
|
||||||
|
|
||||||
|
Future<String?> _restore() async {
|
||||||
|
_accessToken = await _identityStorage.restoreAccessToken();
|
||||||
|
_generation++;
|
||||||
|
return _accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> save(String token) async {
|
||||||
|
await _serialize(() async {
|
||||||
|
await _identityStorage.saveJwt(token);
|
||||||
|
_accessToken = token.trim();
|
||||||
|
_generation++;
|
||||||
|
_restoreFuture = Future<String?>.value(_accessToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear({bool accountDeleted = false}) async {
|
||||||
|
await _serialize(() async {
|
||||||
|
if (accountDeleted) {
|
||||||
|
await _identityStorage.clearAccountCredentials();
|
||||||
|
} else {
|
||||||
|
await _identityStorage.clearSessionCredentials();
|
||||||
|
}
|
||||||
|
_accessToken = null;
|
||||||
|
_generation++;
|
||||||
|
_restoreFuture = Future<String?>.value();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _serialize<T>(Future<T> Function() operation) {
|
||||||
|
final result = _operationTail.then((_) => operation());
|
||||||
|
_operationTail = result.then<void>(
|
||||||
|
(_) {},
|
||||||
|
onError: (Object _, StackTrace _) {},
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final authSessionStorageProvider = Provider<AuthSessionStorage>(
|
||||||
|
(ref) => AuthSessionStorage(ref.watch(secureIdentityStorageProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
String? _normalized(String? value) {
|
String? _normalized(String? value) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const storageThemeMode = 'theme_mode';
|
|||||||
const storagePrivacyAgreed = 'privacy_agreed'; // Android 首启隐私同意(设备采集门控)
|
const storagePrivacyAgreed = 'privacy_agreed'; // Android 首启隐私同意(设备采集门控)
|
||||||
const storageIosStartupSeen = 'ios_startup_seen'; // iOS 首次启动页已展示
|
const storageIosStartupSeen = 'ios_startup_seen'; // iOS 首次启动页已展示
|
||||||
const storageJinzhiHoldings = 'jinzhi_holdings';
|
const storageJinzhiHoldings = 'jinzhi_holdings';
|
||||||
|
const storageJinzhiHoldingsDeleted = 'jinzhi_holdings_deleted';
|
||||||
const storageJinzhiAmountHidden = 'jinzhi_amount_hidden';
|
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';
|
||||||
@@ -18,6 +19,16 @@ const storageMallHomeBadgeSeen = 'jinzhi_mall_home_badge_seen';
|
|||||||
const storageLegacyAccessToken = 'access_token';
|
const storageLegacyAccessToken = 'access_token';
|
||||||
const storageAuthSessionActive = 'auth_session_active';
|
const storageAuthSessionActive = 'auth_session_active';
|
||||||
|
|
||||||
|
// Account-scoped secure values. They are intentionally listed even before
|
||||||
|
// every feature is implemented so account deletion has one complete cleanup
|
||||||
|
// boundary. Access tokens must never be reused as biometric credentials.
|
||||||
|
const secureAccessToken = 'access_token';
|
||||||
|
const secureLegacyJwtToken = 'jwt_token';
|
||||||
|
const secureDeviceId = 'device_id';
|
||||||
|
const secureMallCredential = 'mall_credential';
|
||||||
|
const secureRefreshToken = 'refresh_token';
|
||||||
|
const secureBiometricCredential = 'biometric_credential';
|
||||||
|
|
||||||
// 设备/广告标识。由各自的 SDK/ATT 流程写入,DeviceHeaderService 读出注入
|
// 设备/广告标识。由各自的 SDK/ATT 流程写入,DeviceHeaderService 读出注入
|
||||||
// header;未写入则为空、注入时被过滤。
|
// header;未写入则为空、注入时被过滤。
|
||||||
const storageDeviceToken = 'device_token'; // 推送 token
|
const storageDeviceToken = 'device_token'; // 推送 token
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class AppNetworkImage extends StatelessWidget {
|
|||||||
this.alignment = Alignment.center,
|
this.alignment = Alignment.center,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
this.cacheKey,
|
this.cacheKey,
|
||||||
this.httpHeaders,
|
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.errorWidget,
|
this.errorWidget,
|
||||||
this.semanticLabel,
|
this.semanticLabel,
|
||||||
@@ -29,7 +28,6 @@ class AppNetworkImage extends StatelessWidget {
|
|||||||
final Alignment alignment;
|
final Alignment alignment;
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
final String? cacheKey;
|
final String? cacheKey;
|
||||||
final Map<String, String>? httpHeaders;
|
|
||||||
final Widget? placeholder;
|
final Widget? placeholder;
|
||||||
final Widget? errorWidget;
|
final Widget? errorWidget;
|
||||||
final String? semanticLabel;
|
final String? semanticLabel;
|
||||||
@@ -41,7 +39,6 @@ class AppNetworkImage extends StatelessWidget {
|
|||||||
: CachedNetworkImage(
|
: CachedNetworkImage(
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
cacheKey: cacheKey,
|
cacheKey: cacheKey,
|
||||||
httpHeaders: httpHeaders,
|
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../network/api_error_message.dart';
|
||||||
import 'app_state_card.dart';
|
import 'app_state_card.dart';
|
||||||
|
|
||||||
/// Shared loading, failure and empty-state contract for feature pages.
|
/// Shared loading, failure and empty-state contract for feature pages.
|
||||||
@@ -37,11 +38,11 @@ class AsyncStateView<T> extends StatelessWidget {
|
|||||||
return state.when(
|
return state.when(
|
||||||
loading: () =>
|
loading: () =>
|
||||||
loading ?? const Center(child: CircularProgressIndicator()),
|
loading ?? const Center(child: CircularProgressIndicator()),
|
||||||
error: (_, _) => Center(
|
error: (error, _) => Center(
|
||||||
child: AppStateCard(
|
child: AppStateCard(
|
||||||
icon: Icons.error_outline,
|
icon: Icons.error_outline,
|
||||||
title: errorTitle,
|
title: errorTitle,
|
||||||
subtitle: errorSubtitle,
|
subtitle: ErrorMessageMapper.message(error, fallback: errorSubtitle),
|
||||||
buttonLabel: onRetry == null ? null : '重试',
|
buttonLabel: onRetry == null ? null : '重试',
|
||||||
onButtonPressed: onRetry,
|
onButtonPressed: onRetry,
|
||||||
buttonStyle: AppStateButtonStyle.filled,
|
buttonStyle: AppStateButtonStyle.filled,
|
||||||
@@ -60,3 +61,44 @@ class AsyncStateView<T> extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sliver counterpart for CustomScrollView pages without forcing one global
|
||||||
|
/// card layout on successful feature content.
|
||||||
|
class SliverAsyncState<T> extends StatelessWidget {
|
||||||
|
const SliverAsyncState({
|
||||||
|
super.key,
|
||||||
|
required this.state,
|
||||||
|
required this.data,
|
||||||
|
this.loading,
|
||||||
|
this.onRetry,
|
||||||
|
this.errorSubtitle = '请检查网络后重试',
|
||||||
|
});
|
||||||
|
|
||||||
|
final AsyncValue<T> state;
|
||||||
|
final Widget Function(T value) data;
|
||||||
|
final Widget? loading;
|
||||||
|
final VoidCallback? onRetry;
|
||||||
|
final String errorSubtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => state.when(
|
||||||
|
data: data,
|
||||||
|
loading: () => SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: loading ?? const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error: (error, _) => SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(
|
||||||
|
child: AppStateCard(
|
||||||
|
icon: Icons.error_outline,
|
||||||
|
title: '加载失败',
|
||||||
|
subtitle: ErrorMessageMapper.message(error, fallback: errorSubtitle),
|
||||||
|
buttonLabel: onRetry == null ? null : '重试',
|
||||||
|
onButtonPressed: onRetry,
|
||||||
|
buttonStyle: AppStateButtonStyle.filled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import 'dart:io' show File;
|
|||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../../common/storage/storage_service.dart';
|
import '../../../common/storage/preferences_storage.dart';
|
||||||
import '../data/ads_api_client.dart';
|
import '../data/ads_api_client.dart';
|
||||||
|
|
||||||
class SplashAdEntry {
|
class SplashAdEntry {
|
||||||
@@ -33,12 +33,13 @@ class SplashAdEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SplashAdController {
|
class SplashAdController {
|
||||||
SplashAdController(this._api);
|
SplashAdController(this._api, this._preferences);
|
||||||
final AdsApiClient _api;
|
final AdsApiClient _api;
|
||||||
|
final PreferencesStorage _preferences;
|
||||||
static const _entryKey = 'ads.splash.entry';
|
static const _entryKey = 'ads.splash.entry';
|
||||||
static const _dailyKey = 'ads.splash.daily';
|
static const _dailyKey = 'ads.splash.daily';
|
||||||
Future<SplashAdEntry?> coldLaunchDecision() async {
|
Future<SplashAdEntry?> coldLaunchDecision() async {
|
||||||
final raw = StorageService.to.preferences.getString(_entryKey);
|
final raw = _preferences.getString(_entryKey);
|
||||||
if (raw == null) return null;
|
if (raw == null) return null;
|
||||||
try {
|
try {
|
||||||
final entry = SplashAdEntry.fromJson(
|
final entry = SplashAdEntry.fromJson(
|
||||||
@@ -55,7 +56,7 @@ class SplashAdController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _underCap(SplashAdEntry entry) {
|
bool _underCap(SplashAdEntry entry) {
|
||||||
final value = StorageService.to.preferences.getString(_dailyKey);
|
final value = _preferences.getString(_dailyKey);
|
||||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||||
if (value == null) return true;
|
if (value == null) return true;
|
||||||
final map = Map<String, dynamic>.from(jsonDecode(value) as Map);
|
final map = Map<String, dynamic>.from(jsonDecode(value) as Map);
|
||||||
@@ -68,13 +69,13 @@ class SplashAdController {
|
|||||||
entry.assetPath != null && await File(entry.assetPath!).exists();
|
entry.assetPath != null && await File(entry.assetPath!).exists();
|
||||||
Future<void> markShown(SplashAdEntry entry) async {
|
Future<void> markShown(SplashAdEntry entry) async {
|
||||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||||
final raw = StorageService.to.preferences.getString(_dailyKey);
|
final raw = _preferences.getString(_dailyKey);
|
||||||
var count = 0;
|
var count = 0;
|
||||||
if (raw != null) {
|
if (raw != null) {
|
||||||
final m = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
final m = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
||||||
if (m['date'] == today) count = (m['count'] as num?)?.toInt() ?? 0;
|
if (m['date'] == today) count = (m['count'] as num?)?.toInt() ?? 0;
|
||||||
}
|
}
|
||||||
await StorageService.to.preferences.setString(
|
await _preferences.setString(
|
||||||
_dailyKey,
|
_dailyKey,
|
||||||
jsonEncode({'date': today, 'count': count + 1}),
|
jsonEncode({'date': today, 'count': count + 1}),
|
||||||
);
|
);
|
||||||
@@ -92,7 +93,7 @@ class SplashAdController {
|
|||||||
result.ad!.imageUrl,
|
result.ad!.imageUrl,
|
||||||
);
|
);
|
||||||
if (!await file.exists()) return;
|
if (!await file.exists()) return;
|
||||||
await StorageService.to.preferences.setString(
|
await _preferences.setString(
|
||||||
_entryKey,
|
_entryKey,
|
||||||
jsonEncode(
|
jsonEncode(
|
||||||
SplashAdEntry(
|
SplashAdEntry(
|
||||||
@@ -115,14 +116,17 @@ class SplashAdController {
|
|||||||
Future<void> skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip');
|
Future<void> skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip');
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
await _clearEntry();
|
await _clearEntry();
|
||||||
await StorageService.to.preferences.remove(_dailyKey);
|
await _preferences.remove(_dailyKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _clearEntry() async {
|
Future<void> _clearEntry() async {
|
||||||
await StorageService.to.preferences.remove(_entryKey);
|
await _preferences.remove(_entryKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final splashAdControllerProvider = Provider(
|
final splashAdControllerProvider = Provider(
|
||||||
(ref) => SplashAdController(ref.watch(adsApiClientProvider)),
|
(ref) => SplashAdController(
|
||||||
|
ref.watch(adsApiClientProvider),
|
||||||
|
ref.watch(preferencesStorageProvider),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../../common/router/app_router.dart';
|
import '../../../common/router/app_router.dart';
|
||||||
import '../data/ads_api_client.dart';
|
import '../data/ads_api_client.dart';
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
// ignore: unused_import — compute() is referenced by generated Retrofit code.
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:retrofit/retrofit.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;
|
||||||
|
import '../../../models/pb/jinzhi/v1/ads.pb.dart'
|
||||||
|
show GetAdReply, ListAdsReply, ReportAdEventReply, ReportAdEventRequest;
|
||||||
|
|
||||||
|
part 'ads_api_client.g.dart';
|
||||||
|
|
||||||
const splashAdPlacement = 'splash';
|
const splashAdPlacement = 'splash';
|
||||||
const homeBannerAdPlacement = 'home_banner';
|
const homeBannerAdPlacement = 'home_banner';
|
||||||
@@ -100,24 +106,26 @@ class AdCreative {
|
|||||||
? AdOpenMode.inApp
|
? AdOpenMode.inApp
|
||||||
: AdOpenMode.external,
|
: AdOpenMode.external,
|
||||||
);
|
);
|
||||||
factory AdCreative.fromProto(ProtoMessage fields) => AdCreative(
|
factory AdCreative.fromProto(pb.AdCreative creative) => AdCreative(
|
||||||
adId: fields.string(1),
|
adId: creative.adId,
|
||||||
placement: fields.string(2),
|
placement: creative.placement,
|
||||||
targetApp: fields.string(3),
|
targetApp: creative.targetApp,
|
||||||
title: fields.string(4),
|
title: creative.title,
|
||||||
imageUrl: fields.string(6),
|
imageUrl: creative.imageUrl,
|
||||||
mediaType: switch (fields.integer(7)) {
|
mediaType: switch (creative.mediaType) {
|
||||||
1 => AdMediaType.image,
|
pb.MediaType.MEDIA_TYPE_IMAGE => AdMediaType.image,
|
||||||
2 => AdMediaType.gif,
|
pb.MediaType.MEDIA_TYPE_GIF => AdMediaType.gif,
|
||||||
3 => AdMediaType.video,
|
pb.MediaType.MEDIA_TYPE_VIDEO => AdMediaType.video,
|
||||||
_ => AdMediaType.unsupported,
|
_ => AdMediaType.unsupported,
|
||||||
},
|
},
|
||||||
h5Url: fields.string(9),
|
h5Url: creative.h5Url,
|
||||||
trackingId: fields.string(10),
|
trackingId: creative.trackingId,
|
||||||
countdownSeconds: fields.integer(11),
|
countdownSeconds: creative.countdownSeconds,
|
||||||
endAtMs: fields.integer(13),
|
endAtMs: creative.endAtMs.toInt(),
|
||||||
trackedUrl: fields.string(14),
|
trackedUrl: creative.trackedUrl,
|
||||||
openMode: fields.integer(15) == 2 ? AdOpenMode.inApp : AdOpenMode.external,
|
openMode: creative.openMode == pb.AdOpenMode.AD_OPEN_MODE_IN_APP
|
||||||
|
? AdOpenMode.inApp
|
||||||
|
: AdOpenMode.external,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,41 +144,49 @@ class FeaturedAdsState {
|
|||||||
bool get hasAds => enabled && ads.isNotEmpty;
|
bool get hasAds => enabled && ads.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RestApi()
|
||||||
|
abstract class AdsBackendApiClient {
|
||||||
|
factory AdsBackendApiClient(Dio dio, {String? baseUrl}) =
|
||||||
|
_AdsBackendApiClient;
|
||||||
|
|
||||||
|
@GET('/api/v1/ads')
|
||||||
|
Future<pb.GetAdReply> getAd({@Query('placement') required String placement});
|
||||||
|
|
||||||
|
@GET('/api/v1/ads/list')
|
||||||
|
Future<pb.ListAdsReply> listAds({
|
||||||
|
@Query('placement') required String placement,
|
||||||
|
});
|
||||||
|
|
||||||
|
@POST('/api/v1/ads/events')
|
||||||
|
Future<pb.ReportAdEventReply> reportAdEvent(
|
||||||
|
@Body() pb.ReportAdEventRequest body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class AdsApiClient {
|
class AdsApiClient {
|
||||||
AdsApiClient(this._dio);
|
AdsApiClient(this._api);
|
||||||
final Dio _dio;
|
final AdsBackendApiClient _api;
|
||||||
|
|
||||||
Future<GetAdResult> getAdResult(String placement) async {
|
Future<GetAdResult> getAdResult(String placement) async {
|
||||||
final response = await _dio.get<List<int>>(
|
final reply = await _api.getAd(placement: placement);
|
||||||
'/api/v1/ads',
|
final ad = reply.hasAd() ? AdCreative.fromProto(reply.ad) : null;
|
||||||
queryParameters: {'placement': placement},
|
|
||||||
options: protobufOptions(),
|
|
||||||
);
|
|
||||||
final root = ProtoMessage(response.data ?? const []);
|
|
||||||
final creative = root.message(2);
|
|
||||||
final ad = AdCreative.fromProto(creative);
|
|
||||||
return GetAdResult(
|
return GetAdResult(
|
||||||
root.integer(1) != 0,
|
reply.enabled,
|
||||||
ad.isRenderable ? ad : null,
|
ad?.isRenderable == true ? ad : null,
|
||||||
root.integer(3),
|
reply.dailyCap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FeaturedAdsState> listAds(String placement) async {
|
Future<FeaturedAdsState> listAds(String placement) async {
|
||||||
final response = await _dio.get<List<int>>(
|
final reply = await _api.listAds(placement: placement);
|
||||||
'/api/v1/ads/list',
|
final ads = reply.ads
|
||||||
queryParameters: {'placement': placement},
|
|
||||||
options: protobufOptions(),
|
|
||||||
);
|
|
||||||
final root = ProtoMessage(response.data ?? const []);
|
|
||||||
final ads = root
|
|
||||||
.messages(2)
|
|
||||||
.map(AdCreative.fromProto)
|
.map(AdCreative.fromProto)
|
||||||
.where((ad) => ad.isRenderable)
|
.where((ad) => ad.isRenderable)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
return FeaturedAdsState(
|
return FeaturedAdsState(
|
||||||
root.integer(1) != 0,
|
reply.enabled,
|
||||||
ads,
|
ads,
|
||||||
trackingId: root.string(3).isEmpty ? null : root.string(3),
|
trackingId: reply.trackingId.isEmpty ? null : reply.trackingId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,23 +195,24 @@ class AdsApiClient {
|
|||||||
String event, {
|
String event, {
|
||||||
String? clickId,
|
String? clickId,
|
||||||
String? trackingId,
|
String? trackingId,
|
||||||
}) => _dio.post<List<int>>(
|
}) async {
|
||||||
'/api/v1/ads/events',
|
await _api.reportAdEvent(
|
||||||
data: _Writer()
|
pb.ReportAdEventRequest(
|
||||||
.string(1, event)
|
eventName: event,
|
||||||
.string(2, ad.placement)
|
placement: ad.placement,
|
||||||
.string(3, ad.adId)
|
adId: ad.adId,
|
||||||
.string(4, trackingId ?? ad.trackingId)
|
trackingId: trackingId ?? ad.trackingId,
|
||||||
.string(5, clickId ?? '')
|
clickId: clickId ?? '',
|
||||||
.integer(6, DateTime.now().millisecondsSinceEpoch)
|
clientTsMs: Int64(DateTime.now().millisecondsSinceEpoch),
|
||||||
.toBytes(),
|
),
|
||||||
options: protobufOptions(),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
String newClickId() => const Uuid().v4();
|
String newClickId() => const Uuid().v4();
|
||||||
}
|
}
|
||||||
|
|
||||||
final adsApiClientProvider = Provider(
|
final adsApiClientProvider = Provider(
|
||||||
(ref) => AdsApiClient(ref.watch(dioProvider)),
|
(ref) => AdsApiClient(AdsBackendApiClient(ref.watch(backendDioProvider))),
|
||||||
);
|
);
|
||||||
final featuredAdsProvider = FutureProvider.autoDispose
|
final featuredAdsProvider = FutureProvider.autoDispose
|
||||||
.family<FeaturedAdsState, String>(
|
.family<FeaturedAdsState, String>(
|
||||||
@@ -210,86 +227,3 @@ Uri? _http(String raw) {
|
|||||||
? uri
|
? uri
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProtoMessage {
|
|
||||||
ProtoMessage(this.bytes);
|
|
||||||
final List<int> bytes;
|
|
||||||
Map<int, List<Object>>? _cache;
|
|
||||||
Map<int, List<Object>> get _fields => _cache ??= _read();
|
|
||||||
int integer(int tag) => (_fields[tag]?.lastOrNull as int?) ?? 0;
|
|
||||||
String string(int tag) {
|
|
||||||
final value = _fields[tag]?.lastOrNull;
|
|
||||||
return value is List<int> ? utf8.decode(value, allowMalformed: true) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtoMessage message(int tag) {
|
|
||||||
final value = _fields[tag]?.lastOrNull;
|
|
||||||
return ProtoMessage(value is List<int> ? value : const []);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ProtoMessage> messages(int tag) => (_fields[tag] ?? const [])
|
|
||||||
.whereType<List<int>>()
|
|
||||||
.map(ProtoMessage.new)
|
|
||||||
.toList();
|
|
||||||
Map<int, List<Object>> _read() {
|
|
||||||
final out = <int, List<Object>>{};
|
|
||||||
var i = 0;
|
|
||||||
int v() {
|
|
||||||
var n = 0, s = 0;
|
|
||||||
while (i < bytes.length) {
|
|
||||||
final b = bytes[i++];
|
|
||||||
n |= (b & 127) << s;
|
|
||||||
if (b < 128) return n;
|
|
||||||
s += 7;
|
|
||||||
}
|
|
||||||
throw const FormatException('Invalid protobuf');
|
|
||||||
}
|
|
||||||
|
|
||||||
while (i < bytes.length) {
|
|
||||||
final key = v(), tag = key >> 3;
|
|
||||||
Object value;
|
|
||||||
if ((key & 7) == 0) {
|
|
||||||
value = v();
|
|
||||||
} else if ((key & 7) == 2) {
|
|
||||||
final l = v(), end = i + l;
|
|
||||||
if (end > bytes.length) throw const FormatException('Invalid protobuf');
|
|
||||||
value = bytes.sublist(i, end);
|
|
||||||
i = end;
|
|
||||||
} else {
|
|
||||||
throw const FormatException('Unsupported protobuf');
|
|
||||||
}
|
|
||||||
(out[tag] ??= []).add(value);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on List<Object> {
|
|
||||||
Object? get lastOrNull => isEmpty ? null : last;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Writer {
|
|
||||||
final b = BytesBuilder(copy: false);
|
|
||||||
_Writer string(int t, String value) {
|
|
||||||
final d = value.codeUnits;
|
|
||||||
v((t << 3) | 2);
|
|
||||||
v(d.length);
|
|
||||||
b.add(d);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
_Writer integer(int t, int value) {
|
|
||||||
v(t << 3);
|
|
||||||
v(value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint8List toBytes() => b.toBytes();
|
|
||||||
void v(int n) {
|
|
||||||
while (n > 127) {
|
|
||||||
b.addByte((n & 127) | 128);
|
|
||||||
n >>= 7;
|
|
||||||
}
|
|
||||||
b.addByte(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import '../../../common/widgets/app_network_image.dart';
|
||||||
import '../data/ads_api_client.dart';
|
import '../data/ads_api_client.dart';
|
||||||
|
|
||||||
/// 统一渲染广告图片/GIF/视频;开屏可优先使用已缓存的本地素材。
|
/// 统一渲染广告图片/GIF/视频;开屏可优先使用已缓存的本地素材。
|
||||||
@@ -86,12 +87,12 @@ class _AdMediaState extends State<AdMedia> {
|
|||||||
errorBuilder: (_, _, _) => _error(),
|
errorBuilder: (_, _, _) => _error(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Image.network(
|
return AppNetworkImage(
|
||||||
widget.ad.imageUrl,
|
imageUrl: widget.ad.imageUrl,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
errorBuilder: (_, _, _) => _error(),
|
errorWidget: _error(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final c = player;
|
final c = player;
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
|
import '../../auth/application/auth_controller.dart';
|
||||||
|
import '../../auth/data/auth_models.dart';
|
||||||
import '../../market/data/mock_market_repository.dart';
|
import '../../market/data/mock_market_repository.dart';
|
||||||
import '../data/asset_models.dart';
|
import '../data/asset_models.dart';
|
||||||
import '../data/mock_asset_repository.dart';
|
import '../data/mock_asset_repository.dart';
|
||||||
|
|
||||||
final assetPortfolioControllerProvider =
|
final assetPortfolioControllerProvider =
|
||||||
StateNotifierProvider<AssetPortfolioController, PortfolioSummary>((ref) {
|
StateNotifierProvider<AssetPortfolioController, PortfolioSummary>((ref) {
|
||||||
final holdings = ref.watch(mockAssetRepositoryProvider).loadHoldings();
|
final repository = ref.watch(mockAssetRepositoryProvider);
|
||||||
|
final authState = ref.watch(authProvider).value;
|
||||||
|
final accountScope = switch (authState) {
|
||||||
|
LoggedInAuthState(:final user) => MockAssetRepository.accountScope(
|
||||||
|
userId: user.id,
|
||||||
|
phone: user.phone,
|
||||||
|
),
|
||||||
|
_ => MockAssetRepository.guestAccountScope,
|
||||||
|
};
|
||||||
|
final holdings = repository.loadHoldings(accountScope);
|
||||||
final market = ref.watch(mockMarketRepositoryProvider);
|
final market = ref.watch(mockMarketRepositoryProvider);
|
||||||
return AssetPortfolioController(
|
return AssetPortfolioController(
|
||||||
holdings: holdings,
|
holdings: holdings,
|
||||||
|
repository: repository,
|
||||||
|
accountScope: accountScope,
|
||||||
spotPriceFor: (metal) => market.quoteFor(metal).spotPrice,
|
spotPriceFor: (metal) => market.quoteFor(metal).spotPrice,
|
||||||
changeFor: (metal) => market.quoteFor(metal).changeAmount,
|
changeFor: (metal) => market.quoteFor(metal).changeAmount,
|
||||||
);
|
);
|
||||||
@@ -19,10 +32,14 @@ final assetPortfolioControllerProvider =
|
|||||||
class AssetPortfolioController extends StateNotifier<PortfolioSummary> {
|
class AssetPortfolioController extends StateNotifier<PortfolioSummary> {
|
||||||
AssetPortfolioController({
|
AssetPortfolioController({
|
||||||
required List<GoldAssetHolding> holdings,
|
required List<GoldAssetHolding> holdings,
|
||||||
|
required MockAssetRepository repository,
|
||||||
|
required String accountScope,
|
||||||
required double Function(MetalType metal) spotPriceFor,
|
required double Function(MetalType metal) spotPriceFor,
|
||||||
required double Function(MetalType metal) changeFor,
|
required double Function(MetalType metal) changeFor,
|
||||||
this.recycleDiscount = 0.97,
|
this.recycleDiscount = 0.97,
|
||||||
}) : _holdings = holdings,
|
}) : _holdings = holdings,
|
||||||
|
_repository = repository,
|
||||||
|
_accountScope = accountScope,
|
||||||
_spotPriceFor = spotPriceFor,
|
_spotPriceFor = spotPriceFor,
|
||||||
_changeFor = changeFor,
|
_changeFor = changeFor,
|
||||||
super(
|
super(
|
||||||
@@ -35,12 +52,25 @@ class AssetPortfolioController extends StateNotifier<PortfolioSummary> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final List<GoldAssetHolding> _holdings;
|
final List<GoldAssetHolding> _holdings;
|
||||||
|
final MockAssetRepository _repository;
|
||||||
|
final String _accountScope;
|
||||||
final double Function(MetalType metal) _spotPriceFor;
|
final double Function(MetalType metal) _spotPriceFor;
|
||||||
final double Function(MetalType metal) _changeFor;
|
final double Function(MetalType metal) _changeFor;
|
||||||
final double recycleDiscount;
|
final double recycleDiscount;
|
||||||
|
|
||||||
void addHolding(GoldAssetHolding holding) {
|
Future<void> addHolding(GoldAssetHolding holding) async {
|
||||||
_holdings.add(holding);
|
_holdings.add(holding);
|
||||||
|
await _repository.saveHoldings(_accountScope, _holdings);
|
||||||
|
state = _buildSummary(
|
||||||
|
holdings: _holdings,
|
||||||
|
spotPriceFor: _spotPriceFor,
|
||||||
|
changeFor: _changeFor,
|
||||||
|
recycleDiscount: recycleDiscount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearHoldings() {
|
||||||
|
_holdings.clear();
|
||||||
state = _buildSummary(
|
state = _buildSummary(
|
||||||
holdings: _holdings,
|
holdings: _holdings,
|
||||||
spotPriceFor: _spotPriceFor,
|
spotPriceFor: _spotPriceFor,
|
||||||
|
|||||||
@@ -51,6 +51,45 @@ class GoldAssetHolding {
|
|||||||
final HoldingStatus status;
|
final HoldingStatus status;
|
||||||
|
|
||||||
double materialValue(double spotPrice) => weightGram * purity * spotPrice;
|
double materialValue(double spotPrice) => weightGram * purity * spotPrice;
|
||||||
|
|
||||||
|
Map<String, Object?> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'metal': metal.name,
|
||||||
|
'category': category.name,
|
||||||
|
'purity': purity,
|
||||||
|
'purityLabel': purityLabel,
|
||||||
|
'weightGram': weightGram,
|
||||||
|
'costAmount': costAmount,
|
||||||
|
'purchaseDate': purchaseDate?.toIso8601String(),
|
||||||
|
'channel': channel,
|
||||||
|
'note': note,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
|
'status': status.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory GoldAssetHolding.fromJson(Map<String, Object?> json) {
|
||||||
|
return GoldAssetHolding(
|
||||||
|
id: json['id']! as String,
|
||||||
|
name: json['name']! as String,
|
||||||
|
metal: MetalType.values.byName(json['metal']! as String),
|
||||||
|
category: AssetCategory.values.byName(json['category']! as String),
|
||||||
|
purity: (json['purity']! as num).toDouble(),
|
||||||
|
purityLabel: json['purityLabel']! as String,
|
||||||
|
weightGram: (json['weightGram']! as num).toDouble(),
|
||||||
|
costAmount: (json['costAmount'] as num?)?.toDouble(),
|
||||||
|
purchaseDate: switch (json['purchaseDate']) {
|
||||||
|
final String value when value.isNotEmpty => DateTime.parse(value),
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
channel: json['channel'] as String?,
|
||||||
|
note: json['note'] as String?,
|
||||||
|
createdAt: DateTime.parse(json['createdAt']! as String),
|
||||||
|
updatedAt: DateTime.parse(json['updatedAt']! as String),
|
||||||
|
status: HoldingStatus.values.byName(json['status']! as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HoldingValuation {
|
class HoldingValuation {
|
||||||
|
|||||||
@@ -1,14 +1,91 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
|
import '../../../common/storage/preferences_storage.dart';
|
||||||
|
import '../../../common/storage/storage_keys.dart';
|
||||||
import 'asset_models.dart';
|
import 'asset_models.dart';
|
||||||
|
|
||||||
final mockAssetRepositoryProvider = Provider<MockAssetRepository>(
|
final mockAssetRepositoryProvider = Provider<MockAssetRepository>(
|
||||||
(ref) => MockAssetRepository(),
|
(ref) => MockAssetRepository(ref.watch(preferencesStorageProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
class MockAssetRepository {
|
class MockAssetRepository {
|
||||||
List<GoldAssetHolding> loadHoldings() {
|
MockAssetRepository(this._preferences);
|
||||||
|
|
||||||
|
static const guestAccountScope = 'guest';
|
||||||
|
|
||||||
|
final PreferencesStorage _preferences;
|
||||||
|
Future<void> _operationTail = Future<void>.value();
|
||||||
|
|
||||||
|
static String accountScope({String? userId, String? phone}) {
|
||||||
|
final normalizedUserId = userId?.trim() ?? '';
|
||||||
|
if (normalizedUserId.isNotEmpty) return 'id:$normalizedUserId';
|
||||||
|
final normalizedPhone = phone?.trim() ?? '';
|
||||||
|
if (normalizedPhone.isNotEmpty) return 'phone:$normalizedPhone';
|
||||||
|
return guestAccountScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String holdingsStorageKey(String accountScope) =>
|
||||||
|
'$storageJinzhiHoldings:${Uri.encodeComponent(accountScope)}';
|
||||||
|
|
||||||
|
static String deletedStorageKey(String accountScope) =>
|
||||||
|
'$storageJinzhiHoldingsDeleted:${Uri.encodeComponent(accountScope)}';
|
||||||
|
|
||||||
|
List<GoldAssetHolding> loadHoldings(String accountScope) {
|
||||||
|
if (_preferences.getBool(deletedStorageKey(accountScope))) return [];
|
||||||
|
final encoded = _preferences.getString(holdingsStorageKey(accountScope));
|
||||||
|
if (encoded == null || encoded.isEmpty) return _seedHoldings();
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(encoded);
|
||||||
|
if (decoded is! List) return _seedHoldings();
|
||||||
|
return [
|
||||||
|
for (final item in decoded)
|
||||||
|
GoldAssetHolding.fromJson(Map<String, Object?>.from(item as Map)),
|
||||||
|
];
|
||||||
|
} catch (_) {
|
||||||
|
return _seedHoldings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveHoldings(
|
||||||
|
String accountScope,
|
||||||
|
List<GoldAssetHolding> holdings,
|
||||||
|
) async {
|
||||||
|
final encoded = jsonEncode([
|
||||||
|
for (final holding in holdings) holding.toJson(),
|
||||||
|
]);
|
||||||
|
await _serialize(() async {
|
||||||
|
await _preferences.setString(holdingsStorageKey(accountScope), encoded);
|
||||||
|
await _preferences.remove(deletedStorageKey(accountScope));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAccountHoldings(String accountScope) async {
|
||||||
|
await _serialize(() async {
|
||||||
|
final scopes = {accountScope, guestAccountScope};
|
||||||
|
await Future.wait([
|
||||||
|
for (final scope in scopes) ...[
|
||||||
|
_preferences.remove(holdingsStorageKey(scope)),
|
||||||
|
_preferences.setBool(deletedStorageKey(scope), true),
|
||||||
|
],
|
||||||
|
// Remove the pre-account-scoping key left by earlier builds.
|
||||||
|
_preferences.remove(storageJinzhiHoldings),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _serialize<T>(Future<T> Function() operation) {
|
||||||
|
final result = _operationTail.then((_) => operation());
|
||||||
|
_operationTail = result.then<void>(
|
||||||
|
(_) {},
|
||||||
|
onError: (Object _, StackTrace _) {},
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GoldAssetHolding> _seedHoldings() {
|
||||||
final now = DateTime(2026, 6, 18, 16, 11);
|
final now = DateTime(2026, 6, 18, 16, 11);
|
||||||
return [
|
return [
|
||||||
GoldAssetHolding(
|
GoldAssetHolding(
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
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 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
|
import '../../../common/network/api_exception.dart';
|
||||||
import '../../../common/network/dio_provider.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
import '../../../common/storage/secure_identity_storage.dart';
|
import '../../../common/storage/preferences_storage.dart';
|
||||||
import '../../../common/storage/storage_keys.dart';
|
import '../../../common/storage/storage_keys.dart';
|
||||||
import '../../../common/storage/storage_service.dart';
|
|
||||||
import '../data/auth_api.dart';
|
import '../data/auth_api.dart';
|
||||||
import '../data/auth_models.dart';
|
import '../data/auth_models.dart';
|
||||||
|
import 'auth_session_coordinator.dart';
|
||||||
|
|
||||||
const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in';
|
const _legacyMockLoggedInKey = 'jinzhi_mock_logged_in';
|
||||||
|
|
||||||
@@ -15,26 +16,27 @@ final authProvider =
|
|||||||
StateNotifierProvider<AuthController, AsyncValue<AuthState>>((ref) {
|
StateNotifierProvider<AuthController, AsyncValue<AuthState>>((ref) {
|
||||||
return AuthController(
|
return AuthController(
|
||||||
ref.read(dioProvider),
|
ref.read(dioProvider),
|
||||||
identityStorage: ref.read(secureIdentityStorageProvider),
|
preferences: ref.read(preferencesStorageProvider),
|
||||||
preferences: StorageService.to.preferences,
|
coordinator: ref.read(authSessionCoordinatorProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
||||||
AuthController(
|
AuthController(
|
||||||
this._dio, {
|
this._dio, {
|
||||||
required SecureIdentityStorage identityStorage,
|
required PreferencesStorage preferences,
|
||||||
required SharedPreferences preferences,
|
required AuthSessionCoordinator coordinator,
|
||||||
bool autoBootstrap = true,
|
bool autoBootstrap = true,
|
||||||
}) : _identityStorage = identityStorage,
|
}) : _preferences = preferences,
|
||||||
_preferences = preferences,
|
_coordinator = coordinator,
|
||||||
super(const AsyncLoading()) {
|
super(const AsyncLoading()) {
|
||||||
|
_coordinator.addListener(_onSessionEnded);
|
||||||
if (autoBootstrap) _bootstrap();
|
if (autoBootstrap) _bootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
final SecureIdentityStorage _identityStorage;
|
final PreferencesStorage _preferences;
|
||||||
final SharedPreferences _preferences;
|
final AuthSessionCoordinator _coordinator;
|
||||||
|
|
||||||
AuthApi get _api => AuthApi(_dio);
|
AuthApi get _api => AuthApi(_dio);
|
||||||
|
|
||||||
@@ -42,28 +44,40 @@ class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
|||||||
|
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
final token = await _identityStorage.readJwt();
|
final token = await _coordinator.restore();
|
||||||
if (token == null || token.isEmpty) {
|
if (token == null || token.isEmpty) {
|
||||||
state = const AsyncData(GuestAuthState());
|
state = const AsyncData(GuestAuthState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final cached = _readCachedProfile();
|
||||||
|
if (cached != null) {
|
||||||
|
state = AsyncData(
|
||||||
|
LoggedInAuthState(user: cached, token: token, isVerified: false),
|
||||||
|
);
|
||||||
|
await _persistProfile(cached);
|
||||||
|
}
|
||||||
|
await _verifyProfile(token, _coordinator.generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _verifyProfile(String token, int generation) async {
|
||||||
try {
|
try {
|
||||||
final user = await _api.getProfile();
|
final user = await _api.getProfile();
|
||||||
|
if (generation != _coordinator.generation) return;
|
||||||
await _persistProfile(user);
|
await _persistProfile(user);
|
||||||
state = AsyncData(LoggedInAuthState(user: user, token: token));
|
state = AsyncData(
|
||||||
|
LoggedInAuthState(user: user, token: token, isVerified: true),
|
||||||
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
if (_isUnauthorized(error)) {
|
if (generation != _coordinator.generation) return;
|
||||||
await _clearLocalSession();
|
final exception = ApiException.from(error, stackTrace: stackTrace);
|
||||||
state = const AsyncData(GuestAuthState());
|
if (await _coordinator.expireIfNeeded(exception)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final cached = _canUseCachedProfile(error) ? _readCachedProfile() : null;
|
// Any non-session error (transport, 403, 429, 5xx, decode) preserves a
|
||||||
if (cached != null) {
|
// stale cached session. With no profile there is nothing safe to render,
|
||||||
await _persistProfile(cached);
|
// but the credential still remains available for retry.
|
||||||
state = AsyncData(LoggedInAuthState(user: cached, token: token));
|
if (state.value is LoggedInAuthState) return;
|
||||||
} else {
|
state = AsyncError(exception, stackTrace);
|
||||||
state = AsyncError(error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +94,7 @@ class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _persistLogin(AuthLoginResult reply) async {
|
Future<void> _persistLogin(AuthLoginResult reply) async {
|
||||||
await _identityStorage.saveJwt(reply.accessToken);
|
await _coordinator.startSession(reply.accessToken);
|
||||||
await _persistProfile(reply.user);
|
await _persistProfile(reply.user);
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
LoggedInAuthState(user: reply.user, token: reply.accessToken),
|
LoggedInAuthState(user: reply.user, token: reply.accessToken),
|
||||||
@@ -88,8 +102,7 @@ class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await _clearLocalSession();
|
await _coordinator.logout();
|
||||||
state = const AsyncData(GuestAuthState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAccount(String code) async {
|
Future<void> deleteAccount(String code) async {
|
||||||
@@ -98,8 +111,7 @@ class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
|||||||
throw ArgumentError.value(code, 'code', 'must not be empty');
|
throw ArgumentError.value(code, 'code', 'must not be empty');
|
||||||
}
|
}
|
||||||
await _api.deleteAccount(normalized);
|
await _api.deleteAccount(normalized);
|
||||||
await _clearLocalSession();
|
await _coordinator.accountDeleted();
|
||||||
state = const AsyncData(GuestAuthState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _persistProfile(JinzhiUser user) async {
|
Future<void> _persistProfile(JinzhiUser user) async {
|
||||||
@@ -122,22 +134,19 @@ class AuthController extends StateNotifier<AsyncValue<AuthState>> {
|
|||||||
return JinzhiUser(id: id, phone: phone, createdAt: createdAt);
|
return JinzhiUser(id: id, phone: phone, createdAt: createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _clearLocalSession() async {
|
void _onSessionEnded(SessionEndReason reason) {
|
||||||
await _identityStorage.clearJwt();
|
state = AsyncData(
|
||||||
await _preferences.remove(storageUserId);
|
reason == SessionEndReason.expired
|
||||||
await _preferences.remove(storageUserPhone);
|
? const SessionExpiredAuthState()
|
||||||
await _preferences.remove(storageUserCreatedAt);
|
: const GuestAuthState(),
|
||||||
await _preferences.remove(_legacyMockLoggedInKey);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isLoggedIn => state.valueOrNull is LoggedInAuthState;
|
bool get isLoggedIn => state.value is LoggedInAuthState;
|
||||||
}
|
|
||||||
|
|
||||||
bool _isUnauthorized(Object error) =>
|
@override
|
||||||
error is DioException && error.response?.statusCode == 401;
|
void dispose() {
|
||||||
|
_coordinator.removeListener(_onSessionEnded);
|
||||||
bool _canUseCachedProfile(Object error) {
|
super.dispose();
|
||||||
if (error is! DioException) return false;
|
}
|
||||||
final status = error.response?.statusCode;
|
|
||||||
return status == null || status == 408 || status == 429 || status >= 500;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../common/network/api_exception.dart';
|
||||||
|
import '../../../common/storage/preferences_storage.dart';
|
||||||
|
import '../../../common/storage/secure_identity_storage.dart';
|
||||||
|
import '../../../common/storage/storage_keys.dart';
|
||||||
|
import '../../assets/data/mock_asset_repository.dart';
|
||||||
|
|
||||||
|
enum SessionEndReason { logout, expired, accountDeleted }
|
||||||
|
|
||||||
|
typedef SessionEndListener = void Function(SessionEndReason reason);
|
||||||
|
|
||||||
|
/// Owns session invalidation without depending on Navigator, Router or UI.
|
||||||
|
class AuthSessionCoordinator {
|
||||||
|
AuthSessionCoordinator(
|
||||||
|
this._sessionStorage,
|
||||||
|
this._preferences,
|
||||||
|
this._assetRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
final AuthSessionStorage _sessionStorage;
|
||||||
|
final PreferencesStorage _preferences;
|
||||||
|
final MockAssetRepository _assetRepository;
|
||||||
|
final Set<SessionEndListener> _listeners = {};
|
||||||
|
Future<void>? _ending;
|
||||||
|
SessionEndReason? _endingReason;
|
||||||
|
|
||||||
|
int get generation => _sessionStorage.generation;
|
||||||
|
|
||||||
|
void addListener(SessionEndListener listener) => _listeners.add(listener);
|
||||||
|
void removeListener(SessionEndListener listener) =>
|
||||||
|
_listeners.remove(listener);
|
||||||
|
|
||||||
|
Future<String?> restore() => _sessionStorage.restore();
|
||||||
|
|
||||||
|
Future<void> startSession(String token) async {
|
||||||
|
final ending = _ending;
|
||||||
|
if (ending != null) await ending;
|
||||||
|
await _sessionStorage.save(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> expireIfNeeded(ApiException exception) async {
|
||||||
|
if (!exception.isUnauthorized) return false;
|
||||||
|
final requestGeneration = exception.sessionGeneration;
|
||||||
|
if (requestGeneration != null && requestGeneration != generation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await end(SessionEndReason.expired);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() => end(SessionEndReason.logout);
|
||||||
|
|
||||||
|
Future<void> accountDeleted() => end(SessionEndReason.accountDeleted);
|
||||||
|
|
||||||
|
Future<void> end(SessionEndReason reason) {
|
||||||
|
_endingReason = _strongerReason(_endingReason, reason);
|
||||||
|
final active = _ending;
|
||||||
|
if (active != null) return active;
|
||||||
|
final operation = _drainEnd();
|
||||||
|
late final Future<void> tracked;
|
||||||
|
tracked = operation.whenComplete(() {
|
||||||
|
if (identical(_ending, tracked)) {
|
||||||
|
_ending = null;
|
||||||
|
_endingReason = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_ending = tracked;
|
||||||
|
return tracked;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _drainEnd() async {
|
||||||
|
final assetAccountScope = MockAssetRepository.accountScope(
|
||||||
|
userId: _preferences.getString(storageUserId),
|
||||||
|
phone: _preferences.getString(storageUserPhone),
|
||||||
|
);
|
||||||
|
SessionEndReason? completedReason;
|
||||||
|
while (completedReason != _endingReason) {
|
||||||
|
final reason = _endingReason!;
|
||||||
|
await _sessionStorage.clear(
|
||||||
|
accountDeleted: reason == SessionEndReason.accountDeleted,
|
||||||
|
);
|
||||||
|
await Future.wait([
|
||||||
|
_preferences.remove(storageUserId),
|
||||||
|
_preferences.remove(storageUserPhone),
|
||||||
|
_preferences.remove(storageUserCreatedAt),
|
||||||
|
_preferences.remove('jinzhi_mock_logged_in'),
|
||||||
|
if (reason == SessionEndReason.accountDeleted)
|
||||||
|
_assetRepository.deleteAccountHoldings(assetAccountScope),
|
||||||
|
]);
|
||||||
|
completedReason = reason;
|
||||||
|
}
|
||||||
|
final finalReason = _endingReason!;
|
||||||
|
for (final listener in List<SessionEndListener>.of(_listeners)) {
|
||||||
|
listener(finalReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionEndReason _strongerReason(
|
||||||
|
SessionEndReason? current,
|
||||||
|
SessionEndReason next,
|
||||||
|
) {
|
||||||
|
if (current == null) return next;
|
||||||
|
return _reasonPriority(next) > _reasonPriority(current) ? next : current;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _reasonPriority(SessionEndReason reason) => switch (reason) {
|
||||||
|
SessionEndReason.expired => 0,
|
||||||
|
SessionEndReason.logout => 1,
|
||||||
|
SessionEndReason.accountDeleted => 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
final authSessionCoordinatorProvider = Provider<AuthSessionCoordinator>(
|
||||||
|
(ref) => AuthSessionCoordinator(
|
||||||
|
ref.watch(authSessionStorageProvider),
|
||||||
|
ref.watch(preferencesStorageProvider),
|
||||||
|
ref.watch(mockAssetRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -20,9 +20,18 @@ class GuestAuthState extends AuthState {
|
|||||||
const GuestAuthState();
|
const GuestAuthState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionExpiredAuthState extends AuthState {
|
||||||
|
const SessionExpiredAuthState();
|
||||||
|
}
|
||||||
|
|
||||||
class LoggedInAuthState extends AuthState {
|
class LoggedInAuthState extends AuthState {
|
||||||
const LoggedInAuthState({required this.user, required this.token});
|
const LoggedInAuthState({
|
||||||
|
required this.user,
|
||||||
|
required this.token,
|
||||||
|
this.isVerified = true,
|
||||||
|
});
|
||||||
|
|
||||||
final JinzhiUser user;
|
final JinzhiUser user;
|
||||||
final String token;
|
final String token;
|
||||||
|
final bool isVerified;
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import '../../../common/storage/preferences_storage.dart';
|
||||||
|
|
||||||
import '../../../common/storage/storage_keys.dart';
|
import '../../../common/storage/storage_keys.dart';
|
||||||
|
|
||||||
class MallEntryPromoStore {
|
class MallEntryPromoStore {
|
||||||
const MallEntryPromoStore(this._preferences);
|
const MallEntryPromoStore(this._preferences);
|
||||||
|
|
||||||
final SharedPreferences _preferences;
|
final PreferencesStorage _preferences;
|
||||||
|
|
||||||
bool get hasOpenedMall =>
|
bool get hasOpenedMall => _preferences.getBool(storageMallEntryPromoOpened);
|
||||||
_preferences.getBool(storageMallEntryPromoOpened) ?? false;
|
|
||||||
|
|
||||||
bool get hasSeenHomeBadge =>
|
bool get hasSeenHomeBadge => _preferences.getBool(storageMallHomeBadgeSeen);
|
||||||
_preferences.getBool(storageMallHomeBadgeSeen) ?? false;
|
|
||||||
|
|
||||||
Future<void> markMallOpened() async {
|
Future<void> markMallOpened() async {
|
||||||
await _preferences.setBool(storageMallEntryPromoOpened, true);
|
await _preferences.setBool(storageMallEntryPromoOpened, true);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fixnum/fixnum.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/legacy.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../../../common/network/dio_provider.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../../common/network/dio_provider.dart';
|
import '../../../common/network/dio_provider.dart';
|
||||||
import '../../ads/data/ads_api_client.dart';
|
import '../../../common/network/protobuf_wire_reader.dart';
|
||||||
|
|
||||||
class AppMallAuthToken {
|
class AppMallAuthToken {
|
||||||
const AppMallAuthToken({
|
const AppMallAuthToken({
|
||||||
@@ -33,7 +33,7 @@ class MallAuthApi {
|
|||||||
data: Uint8List(0),
|
data: Uint8List(0),
|
||||||
options: protobufOptions(),
|
options: protobufOptions(),
|
||||||
);
|
);
|
||||||
final reply = ProtoMessage(response.data ?? const <int>[]);
|
final reply = ProtobufWireMessage(response.data ?? const <int>[]);
|
||||||
final token = reply.string(1).trim();
|
final token = reply.string(1).trim();
|
||||||
final expiresIn = reply.integer(2);
|
final expiresIn = reply.integer(2);
|
||||||
final sourceApp = reply.string(3).trim();
|
final sourceApp = reply.string(3).trim();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
import '../data/market_models.dart';
|
import '../data/market_models.dart';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
import '../data/market_models.dart';
|
import '../data/market_models.dart';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
import '../data/mock_news_repository.dart';
|
import '../data/mock_news_repository.dart';
|
||||||
import '../data/news_models.dart';
|
import '../data/news_models.dart';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
import '../data/profile_models.dart';
|
import '../data/profile_models.dart';
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
final settings = ref.watch(profileSettingsControllerProvider);
|
final settings = ref.watch(profileSettingsControllerProvider);
|
||||||
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.value is LoggedInAuthState;
|
||||||
final authLoading = auth.isLoading;
|
final authLoading = auth.isLoading;
|
||||||
final authFailed = auth.hasError;
|
final authFailed = auth.hasError;
|
||||||
final currentUser = auth.valueOrNull is LoggedInAuthState
|
final currentUser = auth.value is LoggedInAuthState
|
||||||
? (auth.valueOrNull as LoggedInAuthState).user
|
? (auth.value as LoggedInAuthState).user
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
import 'package:hooks_riverpod/misc.dart';
|
||||||
|
|
||||||
import '../data/update_api_client.dart';
|
import '../data/update_api_client.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||||
import 'package:flutter_app_update/result_model.dart';
|
import 'package:flutter_app_update/result_model.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:hooks_riverpod/legacy.dart';
|
||||||
|
|
||||||
final updateProgressProvider = StateProvider<double?>((_) => null);
|
final updateProgressProvider = StateProvider<double?>((_) => null);
|
||||||
final updateUrlProvider = StateProvider<String?>((_) => null);
|
final updateUrlProvider = StateProvider<String?>((_) => null);
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class UpgradeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BLOCKED(proto): this endpoint has no authoritative generated Upgrade proto.
|
||||||
|
// Keep its existing wire contract until that source becomes available.
|
||||||
class UpdateApiClient {
|
class UpdateApiClient {
|
||||||
UpdateApiClient(this._dio);
|
UpdateApiClient(this._dio);
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
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);
|
||||||
final loggedIn = authAsync.valueOrNull is LoggedInAuthState;
|
final loggedIn = authAsync.value is LoggedInAuthState;
|
||||||
final updateSupported = UpdateService.isSupported;
|
final updateSupported = UpdateService.isSupported;
|
||||||
final upgrade = ref.watch(updateControllerProvider).valueOrNull;
|
final upgrade = ref.watch(updateControllerProvider).value;
|
||||||
final notificationEnabled = useState(false);
|
final notificationEnabled = useState(false);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (!updateSupported) return null;
|
if (!updateSupported) return null;
|
||||||
@@ -286,7 +286,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
trailing: FeedbackEmailService.recipient,
|
trailing: FeedbackEmailService.recipient,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
try {
|
try {
|
||||||
await FeedbackEmailService.send(authAsync.valueOrNull);
|
await FeedbackEmailService.send(authAsync.value);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (context.mounted) _showFeedbackEmailFailDialog(context);
|
if (context.mounted) _showFeedbackEmailFailDialog(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ class _MallWebHostState extends ConsumerState<MallWebHost>
|
|||||||
next.whenData(_callH5Handler);
|
next.whenData(_callH5Handler);
|
||||||
});
|
});
|
||||||
ref.listenManual<AsyncValue<AuthState>>(authProvider, (previous, next) {
|
ref.listenManual<AsyncValue<AuthState>>(authProvider, (previous, next) {
|
||||||
final previousAuth = previous?.valueOrNull;
|
final previousAuth = previous?.value;
|
||||||
final nextAuth = next.valueOrNull;
|
final nextAuth = next.value;
|
||||||
final wasLogged = previousAuth is LoggedInAuthState;
|
final wasLogged = previousAuth is LoggedInAuthState;
|
||||||
final logged = nextAuth is LoggedInAuthState;
|
final logged = nextAuth is LoggedInAuthState;
|
||||||
final tokenChanged =
|
final tokenChanged =
|
||||||
@@ -550,8 +550,7 @@ class _MallWebHostState extends ConsumerState<MallWebHost>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isLoggedIn =>
|
bool get _isLoggedIn => ref.read(authProvider).value is LoggedInAuthState;
|
||||||
ref.read(authProvider).valueOrNull is LoggedInAuthState;
|
|
||||||
|
|
||||||
Future<void> _notifyH5LoggedOut() async {
|
Future<void> _notifyH5LoggedOut() async {
|
||||||
_isHandlingLoginRequest = false;
|
_isHandlingLoginRequest = false;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -31,32 +33,37 @@ class _AppWebPageState extends State<AppWebPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = WebViewController()
|
_controller =
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
WebViewController(
|
||||||
..addJavaScriptChannel(
|
// Ordinary pages never receive camera/microphone or other protected
|
||||||
'JinzhiBridge',
|
// resources. MallWebHost owns its separate permission policy.
|
||||||
onMessageReceived: (message) {
|
onPermissionRequest: (request) => unawaited(request.deny()),
|
||||||
if (message.message == 'close' && mounted) context.pop();
|
)
|
||||||
},
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
)
|
..addJavaScriptChannel(
|
||||||
..setNavigationDelegate(
|
'JinzhiBridge',
|
||||||
NavigationDelegate(
|
onMessageReceived: (message) {
|
||||||
onProgress: (progress) => setState(() => _progress = progress),
|
if (message.message == 'close' && mounted) context.pop();
|
||||||
onPageFinished: (_) async {
|
},
|
||||||
final title = await _controller.getTitle();
|
)
|
||||||
if (mounted && title != null && title.trim().isNotEmpty) {
|
..setNavigationDelegate(
|
||||||
setState(() => _title = title.trim());
|
NavigationDelegate(
|
||||||
}
|
onProgress: (progress) => setState(() => _progress = progress),
|
||||||
},
|
onPageFinished: (_) async {
|
||||||
onWebResourceError: (error) {
|
final title = await _controller.getTitle();
|
||||||
if (error.isForMainFrame == true && mounted) {
|
if (mounted && title != null && title.trim().isNotEmpty) {
|
||||||
setState(() => _error = error.description);
|
setState(() => _title = title.trim());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigationRequest: _onNavigationRequest,
|
onWebResourceError: (error) {
|
||||||
),
|
if (error.isForMainFrame == true && mounted) {
|
||||||
)
|
setState(() => _error = error.description);
|
||||||
..loadRequest(widget.initialUri);
|
}
|
||||||
|
},
|
||||||
|
onNavigationRequest: _onNavigationRequest,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..loadRequest(widget.initialUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationDecision _onNavigationRequest(NavigationRequest request) {
|
NavigationDecision _onNavigationRequest(NavigationRequest request) {
|
||||||
|
|||||||
+213
-235
@@ -5,32 +5,32 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "85.0.0"
|
version: "100.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.6.0"
|
version: "13.0.0"
|
||||||
analyzer_plugin:
|
analyzer_buffer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_buffer
|
||||||
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
sha256: f890a2e708e38fbf4238ad2f9fd3d506538acdddb0cd8eb2c3e8c7bd17784b57
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.4"
|
version: "0.3.4"
|
||||||
android_id:
|
android_id:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/android_id"
|
path: "packages/android_id"
|
||||||
ref: main
|
ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
resolved-ref: "51fd4ba38d57fdc8fb23e744aa65bf2368878b7f"
|
resolved-ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.4.0"
|
version: "0.4.0"
|
||||||
@@ -46,8 +46,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/android_oaid"
|
path: "packages/android_oaid"
|
||||||
ref: main
|
ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
resolved-ref: "51fd4ba38d57fdc8fb23e744aa65bf2368878b7f"
|
resolved-ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.4.0"
|
version: "0.4.0"
|
||||||
@@ -59,6 +59,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
app_links:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: app_links
|
||||||
|
sha256: "08ba88bf19d9edea33130cb71c8ced2cb982b4e00b82efef4a434d1357bc69c3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.2"
|
||||||
|
app_links_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_linux
|
||||||
|
sha256: "9ccb5802085a1e57bb07b6256fd1d61a78c7ec104944d4965e62938b4fefd9cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
app_links_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_platform_interface
|
||||||
|
sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
|
app_links_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_web
|
||||||
|
sha256: "382d32c6f486f15b1d7f5d6a37a70a481a4fc08382eef419d373169921a3e765"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
app_tracking_transparency:
|
app_tracking_transparency:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -111,18 +143,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "4.0.7"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_config
|
name: build_config
|
||||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.3.3"
|
||||||
build_daemon:
|
build_daemon:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -131,30 +163,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.1.1"
|
||||||
build_resolvers:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_resolvers
|
|
||||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.5.4"
|
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.15.1"
|
||||||
build_runner_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_runner_core
|
|
||||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "9.1.2"
|
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -199,10 +215,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -211,22 +227,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.4"
|
||||||
ci:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ci
|
|
||||||
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.0"
|
|
||||||
cli_util:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cli_util
|
|
||||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.2"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -279,74 +279,50 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
sha256: b7cc2390aac296c083bea762859413eb1cf5b630c70e0c7e4d831018f762a8b2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "2.0.0"
|
||||||
custom_lint:
|
cupertino_ui:
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: custom_lint
|
|
||||||
sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.6"
|
|
||||||
custom_lint_builder:
|
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_builder
|
name: cupertino_ui
|
||||||
sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414"
|
sha256: e9dfe7fac704028f8928cbe4028a0be5e8a709498e8daf8247de99e99a32aef3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "1.0.2"
|
||||||
custom_lint_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: custom_lint_core
|
|
||||||
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.5"
|
|
||||||
custom_lint_visitor:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: custom_lint_visitor
|
|
||||||
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0+7.7.0"
|
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.9"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
|
sha256: ade1c36f8355d6284c98e4a47992ea263887ed3d1acb281e37635dd45f04c4aa
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.3.0"
|
version: "13.3.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: device_info_plus_platform_interface
|
name: device_info_plus_platform_interface
|
||||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.3"
|
version: "8.1.0"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.2"
|
version: "5.11.1"
|
||||||
dio_web_adapter:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -411,6 +387,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
ffi_leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi_leak_tracker
|
||||||
|
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -443,19 +427,20 @@ packages:
|
|||||||
flutter_app_update:
|
flutter_app_update:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_app_update
|
path: "packages/flutter_app_update"
|
||||||
sha256: "336a87c9b8f5c2f040ac314939f61a567e812a3ec3c066d8c99a524d95171d81"
|
ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
url: "https://pub.dev"
|
resolved-ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
source: hosted
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
|
source: git
|
||||||
version: "3.2.4"
|
version: "3.2.4"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
sha256: f94fc6f5480977e3eb7b622ed488f671ba96c971bd0e274e6bd0f0ba9b044816
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
version: "3.4.5"
|
||||||
flutter_dotenv:
|
flutter_dotenv:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -497,24 +482,24 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown_plus
|
name: flutter_markdown_plus
|
||||||
sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de"
|
sha256: fce641d6c2106cc495de1cd603f97a4f9d615a97a64011928ded380dbdade935
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.7"
|
version: "1.0.12"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_native_splash
|
name: flutter_native_splash
|
||||||
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
|
sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.7"
|
version: "2.4.8"
|
||||||
flutter_paid:
|
flutter_paid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/flutter_paid"
|
path: "packages/flutter_paid"
|
||||||
ref: main
|
ref: "47ef2a0ede84de8d0590387f203c72b0ab13f8f8"
|
||||||
resolved-ref: "51fd4ba38d57fdc8fb23e744aa65bf2368878b7f"
|
resolved-ref: "47ef2a0ede84de8d0590387f203c72b0ab13f8f8"
|
||||||
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -530,26 +515,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "3.4.3"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
sha256: d4e1fb6b2cb524868929e78dc0282fa000554b22060fb53789dc481c9fc95bb8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.3.1"
|
version: "11.2.0"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_darwin
|
name: flutter_secure_storage_darwin
|
||||||
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
sha256: a031ceac9b070e62ef183cd7f7d1a8798ebde402d5049f0fe6eb3a51bd797a21
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.2"
|
version: "0.4.3"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -562,10 +547,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_platform_interface
|
name: flutter_secure_storage_platform_interface
|
||||||
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.1.1"
|
||||||
flutter_secure_storage_web:
|
flutter_secure_storage_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -578,10 +563,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_windows
|
name: flutter_secure_storage_windows
|
||||||
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.2.2"
|
||||||
flutter_shaders:
|
flutter_shaders:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -612,10 +597,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2"
|
sha256: a5c39007e19fff5eeb7810d5be651c02bb5a1c06104bdd203b037bbfe49bd827
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.0"
|
version: "10.0.2"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -624,14 +609,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
frontend_server_client:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: frontend_server_client
|
|
||||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.0"
|
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -644,18 +621,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c
|
sha256: d7a3576cb312649eaa51f2356450aed686085fb58fcdebda5b359aa951eef7ea
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.3.0"
|
version: "17.5.0"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_fonts
|
name: google_fonts
|
||||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
sha256: e3cb3ee6b47fd2472c23de6da5744796a4da195137759ddb3fbcc9467b7b3c7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.3"
|
version: "8.2.1"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -668,18 +645,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966"
|
sha256: d58e1e14aa112ca9c56d54effb7d0d717b2d3f96dad3cdd1fce16f8740450700
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "3.4.3"
|
||||||
hotreloader:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: hotreloader
|
|
||||||
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.4.0"
|
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -724,10 +693,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: icons_launcher
|
name: icons_launcher
|
||||||
sha256: "6317d56a73ee528f1dd570d7cd7be120ce58014e0fe635d141ada3d88782f58d"
|
sha256: b42b2f9b10e58d6a973f71293f00a1f0572595e5e50d676c53048e464f78cb7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.1.0"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -824,6 +793,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
listen:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: listen
|
||||||
|
sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -852,26 +829,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
|
material_ui:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_ui
|
||||||
|
sha256: fbfb53cab6c4629438feeade5f3d305f72944bc9d9f25786b19106d32b80ec45
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -880,14 +865,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mockito:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mockito
|
||||||
|
sha256: c8d040d754367108fbe482dcb79dd72b8fe60ac6727abd15b4783c5560297ee6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.7.0"
|
||||||
not_static_icons:
|
not_static_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: not_static_icons
|
name: not_static_icons
|
||||||
sha256: "6150cea030b323eb0d18f9fe7a03d3693dd8a7ba18151fa8e560f099fd3e1db8"
|
sha256: "153102cf0a11534c643d9cbbd0ba09ef82877b2bdb1e5e16b24612ddca5c3a81"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.46.0"
|
version: "0.54.0"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -908,18 +901,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
sha256: "73ef2cd5c11a7548f9b7f6fc2a4f6113de2e64aa2a70de63feede95548822908"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.3.1"
|
version: "10.2.2"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_platform_interface
|
name: package_info_plus_platform_interface
|
||||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "4.1.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -996,18 +989,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: permission_handler
|
name: permission_handler
|
||||||
sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
|
sha256: e6bcecadfe40df6b888141f23cbaf6ba6c3da49fb33f6ff887d060dd48544fbd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.3"
|
version: "13.0.2"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
sha256: b1ce660f4d0dcaffdf2605a19544c129fb5adda4e6fd039992ae356c4e437039
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.1"
|
version: "14.1.0"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1059,10 +1052,10 @@ packages:
|
|||||||
platform_metadata:
|
platform_metadata:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "packages/platform_metadata"
|
||||||
ref: main
|
ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
resolved-ref: bbe1aa0213d3f310f35be59829380ecf01fe9e96
|
resolved-ref: "5c93a6d90977887488d7a73c58083000f1b94f4e"
|
||||||
url: "https://gitea.neuronlabs.art/zhouyu/platform_metadata.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.2+2"
|
version: "0.0.2+2"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
@@ -1093,10 +1086,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: protobuf
|
name: protobuf
|
||||||
sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
|
sha256: "13b6717366aa7ec3792dd62c19c7d33a785708224365f2db23c5ca01f1c1a0f2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "6.1.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1117,58 +1110,50 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: retrofit
|
name: retrofit
|
||||||
sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe"
|
sha256: d700c1e7599bc6327cbe19ec7a2c87b595f6063ce888e8015f30cab655972331
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.1"
|
version: "4.10.0"
|
||||||
retrofit_generator:
|
retrofit_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: retrofit_generator
|
name: retrofit_generator
|
||||||
sha256: "9abcf21acb95bf7040546eafff87f60cf0aee20b05101d71f99876fc4df1f522"
|
sha256: b130dd5a2325ed2bd55dc31a83fe54dd76a87d1a35f1f7d302c8b74b5dbb3d04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.7.0"
|
version: "10.2.9"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "3.4.3"
|
||||||
riverpod_analyzer_utils:
|
riverpod_analyzer_utils:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod_analyzer_utils
|
name: riverpod_analyzer_utils
|
||||||
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
|
sha256: bb13cb3985815840cc103eda1027158358c5fdc00f308fc6f76574d0594107af
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.10"
|
version: "1.0.0-dev.12"
|
||||||
riverpod_annotation:
|
riverpod_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: riverpod_annotation
|
name: riverpod_annotation
|
||||||
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
sha256: "322e56ad198928ef26ba240f85421bfff2493bd65665c887b1299e6386b6206d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "4.0.7"
|
||||||
riverpod_generator:
|
riverpod_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
|
sha256: c94dcb8b60ff4805d2bb8cacca969c48237bcb563e2ab0b5afb50204470c7636
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.5"
|
version: "4.0.9"
|
||||||
riverpod_lint:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: riverpod_lint
|
|
||||||
sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.6.5"
|
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1189,26 +1174,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shadcn_ui
|
name: shadcn_ui
|
||||||
sha256: "6c06f2bcebd8734b9ed0bf3f63ef5c71981573d5664923589b0302f8280e7eaf"
|
sha256: "81fb82da0d56ae836dfec97b3c6a176513af24792c0e21438c16a5d1411ffd9b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.53.6"
|
version: "0.57.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.5"
|
version: "2.5.6"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.23"
|
version: "2.4.28"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1269,18 +1254,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shimmer
|
name: shimmer
|
||||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
sha256: "61ef3b8de6bdb9f16db1bb78d502cb991a12b5223fd2c0b9114d41e782227b55"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "4.0.0"
|
||||||
shorebird_code_push:
|
shorebird_code_push:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shorebird_code_push
|
name: shorebird_code_push
|
||||||
sha256: "55d5eea098a4293199d22dfd263ce1d6c10820ceb42c2acb950f971a8bcee0ce"
|
sha256: "32df9946782c6ca95d7065c7f2156df74675e931b816d0dc391a3d8f8917930f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.7"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1306,10 +1291,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "4.2.4"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1418,10 +1403,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.11"
|
||||||
theme_extensions_builder_annotation:
|
theme_extensions_builder_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1430,14 +1415,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "7.3.0"
|
||||||
timing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: timing
|
|
||||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
two_dimensional_scrollables:
|
two_dimensional_scrollables:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1457,17 +1434,18 @@ packages:
|
|||||||
umeng_common_sdk:
|
umeng_common_sdk:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: umeng_common_sdk
|
path: "packages/umeng_common_sdk"
|
||||||
sha256: "8374ced1ab1d89f596ff71c0eff044d5395337145c9a69a6be02f5d8a678e42c"
|
ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
url: "https://pub.dev"
|
resolved-ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
source: hosted
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
|
source: git
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
umeng_push_sdk:
|
umeng_push_sdk:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/umeng_push_sdk"
|
path: "packages/umeng_push_sdk"
|
||||||
ref: main
|
ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
resolved-ref: "51fd4ba38d57fdc8fb23e744aa65bf2368878b7f"
|
resolved-ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
@@ -1475,8 +1453,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/umeng_verify_sdk"
|
path: "packages/umeng_verify_sdk"
|
||||||
ref: main
|
ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
resolved-ref: "51fd4ba38d57fdc8fb23e744aa65bf2368878b7f"
|
resolved-ref: "97bf7633fe6f9bdde052839432bd7259fb72e757"
|
||||||
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
url: "https://gitea.neuronlabs.art/third-party-project/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
@@ -1500,10 +1478,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1564,10 +1542,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.3"
|
version: "4.6.0"
|
||||||
vector_graphics:
|
vector_graphics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1604,34 +1582,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
|
sha256: "07ae1e03f2d4c8e2e9a75ee678f90acbc4506d4458a9c628512d119a1eee5754"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.1"
|
version: "2.14.1"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_android
|
name: video_player_android
|
||||||
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
sha256: d27054dea34d748a44f06d433a57005d2aac69944485b0abbaed3303dbcfa3f1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.5"
|
version: "2.12.2"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4
|
sha256: "2fc56e537324a19eb649fca991aa308ac961b5406292876fa657b062de461fd8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.9"
|
version: "2.12.0"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_platform_interface
|
name: video_player_platform_interface
|
||||||
sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0"
|
sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.7.0"
|
version: "6.9.0"
|
||||||
video_player_web:
|
video_player_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1684,18 +1662,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter
|
name: webview_flutter
|
||||||
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
|
sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.13.0"
|
version: "4.14.1"
|
||||||
webview_flutter_android:
|
webview_flutter_android:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688
|
sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.12.0"
|
version: "4.14.1"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1708,26 +1686,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: e50cd97ad28e888f6a4f862a05eab917a635417f9b134df64c3abb78250c6446
|
sha256: "0f784be1b080e5b918ef7c2c070c1f4616899fd23f1d001feb74b4978f92bdbc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.25.0"
|
version: "3.26.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.15.0"
|
version: "6.4.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32_registry
|
name: win32_registry
|
||||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "3.0.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1753,5 +1731,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.2 <4.0.0"
|
dart: ">=3.12.2 <4.0.0"
|
||||||
flutter: ">=3.35.6"
|
flutter: ">=3.44.0"
|
||||||
+112
-51
@@ -2,110 +2,171 @@ name: jinzhi_flutter_app
|
|||||||
description: "金值 - 贵金属资产管家"
|
description: "金值 - 贵金属资产管家"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.0+1
|
version: 1.3.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.12.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# 状态管理
|
# 状态管理
|
||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^3.4.3
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^4.0.7
|
||||||
flutter_hooks: ^0.21.3+1
|
flutter_hooks: ^0.21.3+1
|
||||||
|
|
||||||
# 路由
|
# 路由
|
||||||
go_router: ^16.2.4
|
# 与已通过 Android/iOS Release 验证的金小册保持同一 17.x 版本线。
|
||||||
|
# go_router 18.x 留待后续单独做 major 升级验证。
|
||||||
|
go_router: ^17.5.0
|
||||||
|
app_links: ^7.2.2
|
||||||
|
|
||||||
# 网络 + 序列化
|
# 网络 + 序列化
|
||||||
dio: ^5.9.0
|
dio: ^5.11.1
|
||||||
retrofit: ">=4.6.0 <4.9.2"
|
retrofit: ^4.10.0
|
||||||
protobuf: ^4.2.0
|
|
||||||
|
# Checked-in bindings still use PbList, removed by protobuf 4.
|
||||||
|
# Do not upgrade until generated PB bindings are migrated.
|
||||||
|
protobuf: ^6.1.0
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
|
|
||||||
# 存储
|
# 存储
|
||||||
flutter_secure_storage: ^10.3.1
|
flutter_secure_storage: ^11.2.0
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.6
|
||||||
|
|
||||||
|
# Keep pinned for Flutter Built-in Kotlin migration compatibility.
|
||||||
|
shared_preferences_android: 2.4.28
|
||||||
|
|
||||||
# 设备信息
|
# 设备信息
|
||||||
# 12.4.0 requires the iOS 26.1 SDK; 12.3.0 remains compatible with Xcode 26.0.x.
|
device_info_plus: ^13.3.0
|
||||||
device_info_plus: 12.3.0
|
package_info_plus: ^10.2.2
|
||||||
package_info_plus: ^8.3.0
|
uuid: ^4.6.0
|
||||||
uuid: ^4.4.0
|
|
||||||
# UI / 视觉
|
# UI / 视觉
|
||||||
shadcn_ui: ^0.53.6
|
shadcn_ui: ^0.57.1
|
||||||
google_fonts: ^6.2.1
|
|
||||||
|
# Google Fonts 9.x requires Dart 3.13.
|
||||||
|
google_fonts: ^8.2.1
|
||||||
|
|
||||||
flutter_remix: ^0.0.3
|
flutter_remix: ^0.0.3
|
||||||
flutter_svg: ^2.2.3
|
flutter_svg: ^2.3.0
|
||||||
not_static_icons: ^0.46.0
|
not_static_icons: ^0.54.0
|
||||||
shimmer: ^3.0.0
|
shimmer: ^4.0.0
|
||||||
|
|
||||||
# 网络图片与磁盘缓存
|
# 网络图片与磁盘缓存
|
||||||
|
# cached_network_image 暂不跨 major,本轮只升级已验证公共依赖。
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
flutter_cache_manager: ^3.4.1
|
flutter_cache_manager: ^3.4.5
|
||||||
webview_flutter: 4.13.0
|
|
||||||
webview_flutter_android: 4.12.0
|
# WebView
|
||||||
webview_flutter_wkwebview: 3.25.0
|
webview_flutter: ^4.14.1
|
||||||
|
webview_flutter_android: ^4.14.1
|
||||||
|
|
||||||
|
# Latest Dart 3.12-compatible 3.26.x.
|
||||||
|
webview_flutter_wkwebview: ^3.26.2
|
||||||
|
|
||||||
dsbridge_flutter: ^1.2.0
|
dsbridge_flutter: ^1.2.0
|
||||||
flutter_app_update: ^3.2.1
|
|
||||||
android_intent_plus: ^6.0.0
|
# App 更新
|
||||||
permission_handler: ^12.0.1
|
flutter_app_update:
|
||||||
umeng_common_sdk: 1.3.0
|
git:
|
||||||
app_tracking_transparency: ^2.0.6+1
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
flutter_markdown_plus: ^1.0.7
|
path: packages/flutter_app_update
|
||||||
video_player: ^2.10.0 # 启动页视频背景
|
ref: 5c93a6d90977887488d7a73c58083000f1b94f4e
|
||||||
|
|
||||||
|
# 系统 / 权限
|
||||||
|
android_intent_plus: ^6.1.0
|
||||||
|
permission_handler: ^13.0.2
|
||||||
|
app_tracking_transparency: ^2.0.7
|
||||||
|
|
||||||
|
# 内容 / 播放
|
||||||
|
flutter_markdown_plus: ^1.0.12
|
||||||
|
video_player: ^2.14.1
|
||||||
|
|
||||||
# 工具
|
# 工具
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.3
|
||||||
fancy_dio_inspector: ^2.0.1
|
fancy_dio_inspector: ^2.0.1
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
|
# Major upgrade: validate Cupertino icon rendering.
|
||||||
|
cupertino_icons: ^2.0.0
|
||||||
|
|
||||||
|
# 私有 Android 插件
|
||||||
android_id:
|
android_id:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
path: packages/android_id
|
path: packages/android_id
|
||||||
ref: main
|
ref: 5c93a6d90977887488d7a73c58083000f1b94f4e
|
||||||
|
|
||||||
android_oaid:
|
android_oaid:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
path: packages/android_oaid
|
path: packages/android_oaid
|
||||||
ref: main
|
ref: 5c93a6d90977887488d7a73c58083000f1b94f4e
|
||||||
|
|
||||||
flutter_paid:
|
flutter_paid:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
path: packages/flutter_paid
|
path: packages/flutter_paid
|
||||||
ref: main
|
ref: 47ef2a0ede84de8d0590387f203c72b0ab13f8f8
|
||||||
|
|
||||||
platform_metadata:
|
platform_metadata:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/zhouyu/platform_metadata.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
ref: main
|
path: packages/platform_metadata
|
||||||
|
ref: 5c93a6d90977887488d7a73c58083000f1b94f4e
|
||||||
|
|
||||||
|
# 友盟
|
||||||
|
umeng_common_sdk:
|
||||||
|
git:
|
||||||
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
|
path: packages/umeng_common_sdk
|
||||||
|
ref: 97bf7633fe6f9bdde052839432bd7259fb72e757
|
||||||
|
|
||||||
umeng_push_sdk:
|
umeng_push_sdk:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
path: packages/umeng_push_sdk
|
path: packages/umeng_push_sdk
|
||||||
ref: main
|
ref: 97bf7633fe6f9bdde052839432bd7259fb72e757
|
||||||
|
|
||||||
umeng_verify_sdk:
|
umeng_verify_sdk:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
url: https://gitea.neuronlabs.art/third-party-project/flutter-packages.git
|
||||||
path: packages/umeng_verify_sdk
|
path: packages/umeng_verify_sdk
|
||||||
ref: main
|
ref: 97bf7633fe6f9bdde052839432bd7259fb72e757
|
||||||
|
|
||||||
flutter_dotenv: ^6.0.1
|
flutter_dotenv: ^6.0.1
|
||||||
fluttertoast: ^9.0.0
|
fluttertoast: ^10.0.2
|
||||||
shorebird_code_push: ^2.0.6
|
|
||||||
|
shorebird_code_push: ^2.0.7
|
||||||
|
|
||||||
flutter_html: ^3.0.0
|
flutter_html: ^3.0.0
|
||||||
easy_refresh: ^3.5.1
|
easy_refresh: ^3.5.1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.5.4
|
|
||||||
retrofit_generator: ^9.7.0
|
# Flutter 3.44.9 下继续保持。
|
||||||
riverpod_generator: ^2.6.5
|
# 2.15.2+ 会通过 analyzer 要求 meta ^1.18.3,
|
||||||
custom_lint: ^0.7.0
|
# 与 flutter_test 固定的 meta 1.18.0 冲突。
|
||||||
icons_launcher: ^3.0.1
|
build_runner: ^2.15.1
|
||||||
flutter_native_splash: ^2.4.6
|
|
||||||
riverpod_lint: ^2.6.1
|
# 必须精确锁定。
|
||||||
|
# 10.2.10+ 同样会要求新的 analyzer/meta。
|
||||||
|
retrofit_generator: 10.2.9
|
||||||
|
|
||||||
|
riverpod_generator: ^4.0.9
|
||||||
|
|
||||||
|
icons_launcher: ^3.1.0
|
||||||
|
flutter_native_splash: ^2.4.8
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
@@ -113,4 +174,4 @@ flutter:
|
|||||||
- shorebird.yaml
|
- shorebird.yaml
|
||||||
- assets/env/.env
|
- assets/env/.env
|
||||||
- assets/launch/
|
- assets/launch/
|
||||||
- assets/navigation/
|
- assets/navigation/
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/network/api_exception.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('classifies Kratos invalid token and preserves the raw body', () async {
|
||||||
|
final body = errors.Status(
|
||||||
|
code: 401,
|
||||||
|
reason: 'INVALID_TOKEN',
|
||||||
|
message: 'expired',
|
||||||
|
).writeToBuffer();
|
||||||
|
final dio = Dio()
|
||||||
|
..httpClientAdapter = _ErrorAdapter(body, 401)
|
||||||
|
..interceptors.add(KratosErrorInterceptor());
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dio.get<List<int>>(
|
||||||
|
'/me',
|
||||||
|
options: Options(responseType: ResponseType.bytes),
|
||||||
|
);
|
||||||
|
fail('request should fail');
|
||||||
|
} on DioException catch (error) {
|
||||||
|
final exception = ApiException.from(error);
|
||||||
|
expect(exception.kind, ApiExceptionKind.backend);
|
||||||
|
expect(exception.reason, 'INVALID_TOKEN');
|
||||||
|
expect(exception.isUnauthorized, isTrue);
|
||||||
|
expect(error.response!.data, body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('classifies transport, retryable server errors, and cancellation', () {
|
||||||
|
final request = RequestOptions(path: '/test');
|
||||||
|
expect(
|
||||||
|
ApiException.fromDio(DioException(requestOptions: request)).kind,
|
||||||
|
ApiExceptionKind.transport,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ApiException.fromDio(
|
||||||
|
DioException(
|
||||||
|
requestOptions: request,
|
||||||
|
response: Response<void>(requestOptions: request, statusCode: 503),
|
||||||
|
),
|
||||||
|
).isRetryable,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ApiException.fromDio(
|
||||||
|
DioException(requestOptions: request, type: DioExceptionType.cancel),
|
||||||
|
).kind,
|
||||||
|
ApiExceptionKind.cancelled,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorAdapter implements HttpClientAdapter {
|
||||||
|
_ErrorAdapter(this.body, this.status);
|
||||||
|
|
||||||
|
final List<int> body;
|
||||||
|
final int status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ResponseBody> fetch(
|
||||||
|
RequestOptions options,
|
||||||
|
Stream<Uint8List>? requestStream,
|
||||||
|
Future<void>? cancelFuture,
|
||||||
|
) async => ResponseBody.fromBytes(body, status);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool force = false}) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/network/auth_interceptor.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('external absolute URLs lose all backend identity headers', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final session = AuthSessionStorage(
|
||||||
|
SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: _MemorySecureStorage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
|
||||||
|
..interceptors.add(
|
||||||
|
AuthInterceptor(
|
||||||
|
session,
|
||||||
|
backendOrigin: Uri.parse('https://api.example.com'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..httpClientAdapter = _CaptureAdapter();
|
||||||
|
|
||||||
|
final response = await dio.get<void>(
|
||||||
|
'https://cdn.example.net/ad.png',
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer leaked',
|
||||||
|
'X-Device-Id': 'device',
|
||||||
|
'User-iOS-IDFA': 'idfa',
|
||||||
|
'User-DeviceToken': 'push',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final headers = response.requestOptions.headers.keys
|
||||||
|
.map((key) => key.toLowerCase())
|
||||||
|
.toSet();
|
||||||
|
expect(headers, isNot(contains('authorization')));
|
||||||
|
expect(headers, isNot(contains('x-device-id')));
|
||||||
|
expect(headers, isNot(contains('user-ios-idfa')));
|
||||||
|
expect(headers, isNot(contains('user-devicetoken')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CaptureAdapter implements HttpClientAdapter {
|
||||||
|
@override
|
||||||
|
Future<ResponseBody> fetch(
|
||||||
|
RequestOptions options,
|
||||||
|
Stream<Uint8List>? requestStream,
|
||||||
|
Future<void>? cancelFuture,
|
||||||
|
) async => ResponseBody.fromString('', 204);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool force = false}) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/push/push_api_client.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/push.pb.dart' as pb;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'Retrofit push client uses generated protobuf in both directions',
|
||||||
|
() async {
|
||||||
|
final adapter = _PushAdapter();
|
||||||
|
final client = PushApiClient(
|
||||||
|
Dio(BaseOptions(responseType: ResponseType.bytes))
|
||||||
|
..httpClientAdapter = adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final reply = await client.registerDevice(
|
||||||
|
pb.RegisterPushDeviceRequest(
|
||||||
|
umengDeviceToken: 'push-token',
|
||||||
|
platform: pb.Platform.PLATFORM_IOS,
|
||||||
|
appVersion: '1.1.0',
|
||||||
|
permissionGranted: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.deviceRecordId, 'device-record');
|
||||||
|
final request = pb.RegisterPushDeviceRequest.fromBuffer(adapter.body!);
|
||||||
|
expect(request.umengDeviceToken, 'push-token');
|
||||||
|
expect(request.platform, pb.Platform.PLATFORM_IOS);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PushAdapter implements HttpClientAdapter {
|
||||||
|
List<int>? body;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ResponseBody> fetch(
|
||||||
|
RequestOptions options,
|
||||||
|
Stream<Uint8List>? requestStream,
|
||||||
|
Future<void>? cancelFuture,
|
||||||
|
) async {
|
||||||
|
body = (options.data as List<int>).toList();
|
||||||
|
return ResponseBody.fromBytes(
|
||||||
|
pb.RegisterPushDeviceReply(
|
||||||
|
deviceRecordId: 'device-record',
|
||||||
|
).writeToBuffer(),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool force = false}) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/router/deep_link_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(DeepLinkService.resetForTest);
|
||||||
|
|
||||||
|
test('keeps FIFO events until the router is ready', () {
|
||||||
|
DeepLinkService.configureTrustedHttpsHosts({'links.example.com'});
|
||||||
|
DeepLinkService.handle('jinzhi://market');
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://links.example.com/news?id=1'));
|
||||||
|
expect(DeepLinkService.pendingCount, 2);
|
||||||
|
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(navigated, ['/market', '/news?id=1']);
|
||||||
|
expect(DeepLinkService.pendingCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects HTTPS links outside the exact trusted host allowlist', () {
|
||||||
|
DeepLinkService.configureTrustedHttpsHosts({'links.example.com'});
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://evil.example/news'));
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://sub.links.example.com/news'));
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://links.example.com:8443/news'));
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://links.example.com/news'));
|
||||||
|
|
||||||
|
expect(navigated, ['/news']);
|
||||||
|
expect(DeepLinkService.pendingCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disables HTTPS deep links when no production host is configured', () {
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
DeepLinkService.handleUri(Uri.parse('https://links.example.com/news'));
|
||||||
|
DeepLinkService.handle('jinzhi://news');
|
||||||
|
|
||||||
|
expect(navigated, ['/news']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicates event ids even after the first event is consumed', () {
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
final payload = jsonEncode({
|
||||||
|
'event_id': 'same-event',
|
||||||
|
'deep_link': 'jinzhi://assets',
|
||||||
|
});
|
||||||
|
|
||||||
|
DeepLinkService.handle(payload);
|
||||||
|
DeepLinkService.handle(payload);
|
||||||
|
|
||||||
|
expect(navigated, ['/assets']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses bounded queue and discards expired events', () {
|
||||||
|
final old = DateTime.now().subtract(DeepLinkService.expiry * 2);
|
||||||
|
DeepLinkService.enqueue(
|
||||||
|
'jinzhi://assets',
|
||||||
|
source: DeepLinkSource.appLink,
|
||||||
|
receivedAt: old,
|
||||||
|
);
|
||||||
|
for (var index = 0; index < DeepLinkService.maxPending + 5; index++) {
|
||||||
|
DeepLinkService.enqueue(
|
||||||
|
'jinzhi://news?event_id=$index',
|
||||||
|
source: DeepLinkSource.push,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(DeepLinkService.pendingCount, DeepLinkService.maxPending);
|
||||||
|
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
expect(navigated, hasLength(DeepLinkService.maxPending));
|
||||||
|
expect(navigated, isNot(contains('/assets')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth guard leaves the head pending until auth changes', () {
|
||||||
|
var authenticated = false;
|
||||||
|
final navigated = <String>[];
|
||||||
|
DeepLinkService.attachRouter((location) {
|
||||||
|
navigated.add(location);
|
||||||
|
return true;
|
||||||
|
}, authGuard: (_) => authenticated);
|
||||||
|
DeepLinkService.handle('jinzhi://profile');
|
||||||
|
expect(DeepLinkService.pendingCount, 1);
|
||||||
|
|
||||||
|
authenticated = true;
|
||||||
|
DeepLinkService.notifyAuthChanged();
|
||||||
|
expect(navigated, ['/profile']);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.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/secure_identity_storage.dart';
|
||||||
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -14,7 +17,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final preferences = await SharedPreferences.getInstance();
|
||||||
final secure = _MemorySecureStorage();
|
final secure = _MemorySecureStorage();
|
||||||
final storage = SecureIdentityStorage(preferences, secureStorage: secure);
|
final storage = SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
);
|
||||||
|
|
||||||
expect(await storage.restoreAccessToken(), 'legacy');
|
expect(await storage.restoreAccessToken(), 'legacy');
|
||||||
expect(await secure.read('access_token'), 'legacy');
|
expect(await secure.read('access_token'), 'legacy');
|
||||||
@@ -24,17 +30,109 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'migrates the previous secure jwt key when no current token exists',
|
'rejects an orphaned Keychain token when the install marker is absent',
|
||||||
() async {
|
() async {
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final preferences = await SharedPreferences.getInstance();
|
||||||
final secure = _MemorySecureStorage({'jwt_token': 'secure-legacy'});
|
final secure = _MemorySecureStorage({'jwt_token': 'secure-legacy'});
|
||||||
final storage = SecureIdentityStorage(preferences, secureStorage: secure);
|
final storage = SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
);
|
||||||
|
|
||||||
expect(await storage.restoreAccessToken(), 'secure-legacy');
|
expect(await storage.restoreAccessToken(), isNull);
|
||||||
expect(await secure.read('access_token'), 'secure-legacy');
|
expect(await secure.read('access_token'), isNull);
|
||||||
expect(await secure.read('jwt_token'), isNull);
|
expect(await secure.read('jwt_token'), isNull);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('restores a secure token only with the installation marker', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final secure = _MemorySecureStorage({secureAccessToken: 'active'});
|
||||||
|
final storage = SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await storage.restoreAccessToken(), 'active');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout clears session secrets but preserves device identity', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final secure = _MemorySecureStorage({
|
||||||
|
secureAccessToken: 'access',
|
||||||
|
secureMallCredential: 'mall',
|
||||||
|
secureRefreshToken: 'refresh',
|
||||||
|
secureBiometricCredential: 'biometric',
|
||||||
|
secureDeviceId: 'device',
|
||||||
|
});
|
||||||
|
final storage = SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
);
|
||||||
|
|
||||||
|
await storage.clearSessionCredentials();
|
||||||
|
|
||||||
|
expect(await secure.read(secureAccessToken), isNull);
|
||||||
|
expect(await secure.read(secureMallCredential), isNull);
|
||||||
|
expect(await secure.read(secureRefreshToken), isNull);
|
||||||
|
expect(await secure.read(secureBiometricCredential), 'biometric');
|
||||||
|
expect(await secure.read(secureDeviceId), 'device');
|
||||||
|
|
||||||
|
await storage.clearAccountCredentials();
|
||||||
|
expect(await secure.read(secureBiometricCredential), isNull);
|
||||||
|
expect(await secure.read(secureDeviceId), 'device');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'serializes restore with logout so an old token cannot revive',
|
||||||
|
() async {
|
||||||
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final secure = _BlockingReadSecureStorage({
|
||||||
|
secureAccessToken: 'old-token',
|
||||||
|
});
|
||||||
|
final session = AuthSessionStorage(
|
||||||
|
SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final restoring = session.restore();
|
||||||
|
await secure.readStarted.future;
|
||||||
|
final clearing = session.clear();
|
||||||
|
secure.releaseRead.complete();
|
||||||
|
|
||||||
|
expect(await restoring, 'old-token');
|
||||||
|
await clearing;
|
||||||
|
expect(session.accessToken, isNull);
|
||||||
|
expect(await secure.read(secureAccessToken), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('serializes restore with login so the new token wins', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final secure = _BlockingReadSecureStorage({secureAccessToken: 'old-token'});
|
||||||
|
final session = AuthSessionStorage(
|
||||||
|
SecureIdentityStorage(
|
||||||
|
PreferencesStorage(preferences),
|
||||||
|
secureStorage: secure,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final restoring = session.restore();
|
||||||
|
await secure.readStarted.future;
|
||||||
|
final saving = session.save('new-token');
|
||||||
|
secure.releaseRead.complete();
|
||||||
|
|
||||||
|
expect(await restoring, 'old-token');
|
||||||
|
await saving;
|
||||||
|
expect(session.accessToken, 'new-token');
|
||||||
|
expect(await secure.read(secureAccessToken), 'new-token');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MemorySecureStorage implements SecureKeyValueStorage {
|
class _MemorySecureStorage implements SecureKeyValueStorage {
|
||||||
@@ -54,3 +152,21 @@ class _MemorySecureStorage implements SecureKeyValueStorage {
|
|||||||
_values[key] = value;
|
_values[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BlockingReadSecureStorage extends _MemorySecureStorage {
|
||||||
|
_BlockingReadSecureStorage(super.initial);
|
||||||
|
|
||||||
|
final readStarted = Completer<void>();
|
||||||
|
final releaseRead = Completer<void>();
|
||||||
|
var _blocked = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read(String key) async {
|
||||||
|
if (key == secureAccessToken && !_blocked) {
|
||||||
|
_blocked = true;
|
||||||
|
readStarted.complete();
|
||||||
|
await releaseRead.future;
|
||||||
|
}
|
||||||
|
return super.read(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/ads/data/ads_api_client.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'Retrofit client decodes generated ad protobuf and encodes events',
|
||||||
|
() async {
|
||||||
|
final adapter = _AdsAdapter();
|
||||||
|
final backend = AdsBackendApiClient(
|
||||||
|
Dio(BaseOptions(responseType: ResponseType.bytes))
|
||||||
|
..httpClientAdapter = adapter,
|
||||||
|
);
|
||||||
|
final client = AdsApiClient(backend);
|
||||||
|
|
||||||
|
final result = await client.getAdResult(splashAdPlacement);
|
||||||
|
expect(result.enabled, isTrue);
|
||||||
|
expect(result.ad?.title, '商城');
|
||||||
|
expect(result.ad?.endAtMs, 4102444800000);
|
||||||
|
|
||||||
|
await client.report(result.ad!, 'impression');
|
||||||
|
final report = pb.ReportAdEventRequest.fromBuffer(adapter.lastBody!);
|
||||||
|
expect(report.eventName, 'impression');
|
||||||
|
expect(report.adId, 'ad-1');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AdsAdapter implements HttpClientAdapter {
|
||||||
|
List<int>? lastBody;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ResponseBody> fetch(
|
||||||
|
RequestOptions options,
|
||||||
|
Stream<Uint8List>? requestStream,
|
||||||
|
Future<void>? cancelFuture,
|
||||||
|
) async {
|
||||||
|
if (options.path == '/api/v1/ads') {
|
||||||
|
return ResponseBody.fromBytes(
|
||||||
|
pb.GetAdReply(
|
||||||
|
enabled: true,
|
||||||
|
dailyCap: 1,
|
||||||
|
ad: pb.AdCreative(
|
||||||
|
adId: 'ad-1',
|
||||||
|
placement: splashAdPlacement,
|
||||||
|
title: '商城',
|
||||||
|
imageUrl: 'https://cdn.example.com/ad.png',
|
||||||
|
h5Url: 'https://example.com',
|
||||||
|
trackedUrl: 'https://example.com',
|
||||||
|
mediaType: pb.MediaType.MEDIA_TYPE_IMAGE,
|
||||||
|
openMode: pb.AdOpenMode.AD_OPEN_MODE_EXTERNAL,
|
||||||
|
endAtMs: Int64(4102444800000),
|
||||||
|
),
|
||||||
|
).writeToBuffer(),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lastBody = (options.data as List<int>).toList();
|
||||||
|
return ResponseBody.fromBytes(pb.ReportAdEventReply().writeToBuffer(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close({bool force = false}) {}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:jinzhi_flutter_app/features/ads/data/ads_api_client.dart';
|
import 'package:jinzhi_flutter_app/common/network/protobuf_wire_reader.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('protobuf strings are decoded as UTF-8', () {
|
test('protobuf strings are decoded as UTF-8', () {
|
||||||
final value = utf8.encode('商城');
|
final value = utf8.encode('商城');
|
||||||
final message = ProtoMessage(<int>[10, value.length, ...value]);
|
final message = ProtobufWireMessage(<int>[10, value.length, ...value]);
|
||||||
|
|
||||||
expect(message.string(1), '商城');
|
expect(message.string(1), '商城');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/assets/data/mock_asset_repository.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
test('account deletion stays empty after repository recreation', () async {
|
||||||
|
final preferences = PreferencesStorage(
|
||||||
|
await SharedPreferences.getInstance(),
|
||||||
|
);
|
||||||
|
final repository = MockAssetRepository(preferences);
|
||||||
|
final accountScope = MockAssetRepository.accountScope(userId: 'user-1');
|
||||||
|
final initial = repository.loadHoldings(accountScope);
|
||||||
|
expect(initial, isNotEmpty);
|
||||||
|
expect(
|
||||||
|
repository.loadHoldings(MockAssetRepository.guestAccountScope),
|
||||||
|
isNotEmpty,
|
||||||
|
);
|
||||||
|
await repository.saveHoldings(accountScope, initial);
|
||||||
|
|
||||||
|
await repository.deleteAccountHoldings(accountScope);
|
||||||
|
final recreated = MockAssetRepository(preferences);
|
||||||
|
|
||||||
|
expect(recreated.loadHoldings(accountScope), isEmpty);
|
||||||
|
expect(
|
||||||
|
recreated.loadHoldings(MockAssetRepository.guestAccountScope),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('holdings are isolated by account and deletion is scoped', () async {
|
||||||
|
final preferences = PreferencesStorage(
|
||||||
|
await SharedPreferences.getInstance(),
|
||||||
|
);
|
||||||
|
final repository = MockAssetRepository(preferences);
|
||||||
|
final first = MockAssetRepository.accountScope(userId: 'user-1');
|
||||||
|
final second = MockAssetRepository.accountScope(userId: 'user-2');
|
||||||
|
final firstHoldings = repository.loadHoldings(first);
|
||||||
|
final secondHoldings = repository.loadHoldings(second);
|
||||||
|
await repository.saveHoldings(first, firstHoldings.take(1).toList());
|
||||||
|
await repository.saveHoldings(second, secondHoldings.take(2).toList());
|
||||||
|
|
||||||
|
await repository.deleteAccountHoldings(first);
|
||||||
|
|
||||||
|
expect(repository.loadHoldings(first), isEmpty);
|
||||||
|
expect(repository.loadHoldings(second), hasLength(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/secure_identity_storage.dart';
|
||||||
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
|
||||||
import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart';
|
import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/auth/application/auth_session_coordinator.dart';
|
||||||
import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart';
|
import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/assets/data/mock_asset_repository.dart';
|
||||||
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ void main() {
|
|||||||
|
|
||||||
await fixture.controller.retryBootstrap();
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
expect(fixture.controller.state.value, isA<GuestAuthState>());
|
expect(fixture.controller.state.value, isA<SessionExpiredAuthState>());
|
||||||
expect(await fixture.identity.readJwt(), isNull);
|
expect(await fixture.identity.readJwt(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,6 +86,59 @@ void main() {
|
|||||||
expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse);
|
expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('restores cached profile before the background profile reply', () async {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
await fixture.preferences.setString(storageUserId, 'cached-user');
|
||||||
|
await fixture.preferences.setString(storageUserPhone, '13900139000');
|
||||||
|
fixture.profileCompleter = Completer<void>();
|
||||||
|
|
||||||
|
final bootstrap = fixture.controller.retryBootstrap();
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
|
final stale = fixture.controller.state.value! as LoggedInAuthState;
|
||||||
|
expect(stale.user.id, 'cached-user');
|
||||||
|
expect(stale.isVerified, isFalse);
|
||||||
|
|
||||||
|
fixture.profileCompleter!.complete();
|
||||||
|
await bootstrap;
|
||||||
|
expect(
|
||||||
|
(fixture.controller.state.value! as LoggedInAuthState).isVerified,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps cached login on 403, 429, 5xx, and decode failures', () async {
|
||||||
|
for (final failure in [403, 429, 503, -1]) {
|
||||||
|
final fixture = await _Fixture.create();
|
||||||
|
await fixture.identity.saveJwt('token');
|
||||||
|
await fixture.preferences.setString(storageUserId, 'cached-user');
|
||||||
|
fixture.profileFailureStatus = failure;
|
||||||
|
|
||||||
|
await fixture.controller.retryBootstrap();
|
||||||
|
|
||||||
|
expect(fixture.controller.state.value, isA<LoggedInAuthState>());
|
||||||
|
expect(await fixture.identity.readJwt(), 'token');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout preserves assets while account deletion removes them', () async {
|
||||||
|
final logoutFixture = await _Fixture.create();
|
||||||
|
await logoutFixture.identity.saveJwt('token');
|
||||||
|
await logoutFixture.preferences.setString(storageJinzhiHoldings, 'assets');
|
||||||
|
await logoutFixture.controller.logout();
|
||||||
|
expect(
|
||||||
|
logoutFixture.preferences.getString(storageJinzhiHoldings),
|
||||||
|
'assets',
|
||||||
|
);
|
||||||
|
|
||||||
|
final deleteFixture = await _Fixture.create();
|
||||||
|
await deleteFixture.identity.saveJwt('token');
|
||||||
|
await deleteFixture.preferences.setString(storageJinzhiHoldings, 'assets');
|
||||||
|
await deleteFixture.controller.deleteAccount('123456');
|
||||||
|
expect(deleteFixture.preferences.getString(storageJinzhiHoldings), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
test('keeps the verified one-key login state shape', () async {
|
test('keeps the verified one-key login state shape', () async {
|
||||||
final fixture = await _Fixture.create();
|
final fixture = await _Fixture.create();
|
||||||
|
|
||||||
@@ -106,20 +164,47 @@ class _Fixture {
|
|||||||
bool failDeletion = false;
|
bool failDeletion = false;
|
||||||
bool failProfile = false;
|
bool failProfile = false;
|
||||||
bool profileUnauthorized = false;
|
bool profileUnauthorized = false;
|
||||||
|
int? profileFailureStatus;
|
||||||
|
Completer<void>? profileCompleter;
|
||||||
String? deletedWithCode;
|
String? deletedWithCode;
|
||||||
|
|
||||||
static Future<_Fixture> create() async {
|
static Future<_Fixture> create() async {
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final preferences = await SharedPreferences.getInstance();
|
||||||
final identity = SecureIdentityStorage(
|
final identity = SecureIdentityStorage(
|
||||||
preferences,
|
PreferencesStorage(preferences),
|
||||||
secureStorage: _MemorySecureStorage(),
|
secureStorage: _MemorySecureStorage(),
|
||||||
);
|
);
|
||||||
late _Fixture fixture;
|
late _Fixture fixture;
|
||||||
final dio = Dio()
|
final dio = Dio()
|
||||||
..interceptors.add(
|
..interceptors.add(
|
||||||
InterceptorsWrapper(
|
InterceptorsWrapper(
|
||||||
onRequest: (options, handler) {
|
onRequest: (options, handler) async {
|
||||||
if (options.path == '/api/v1/me' && options.method == 'GET') {
|
if (options.path == '/api/v1/me' && options.method == 'GET') {
|
||||||
|
if (fixture.profileCompleter case final completer?) {
|
||||||
|
await completer.future;
|
||||||
|
}
|
||||||
|
if (fixture.profileFailureStatus case final status?) {
|
||||||
|
if (status == -1) {
|
||||||
|
handler.resolve(
|
||||||
|
Response<List<int>>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: 200,
|
||||||
|
data: const [255],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: options,
|
||||||
|
response: Response<void>(
|
||||||
|
requestOptions: options,
|
||||||
|
statusCode: status,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (fixture.failProfile) {
|
if (fixture.failProfile) {
|
||||||
handler.reject(DioException(requestOptions: options));
|
handler.reject(DioException(requestOptions: options));
|
||||||
return;
|
return;
|
||||||
@@ -185,10 +270,17 @@ class _Fixture {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final preferencesStorage = PreferencesStorage(preferences);
|
||||||
|
final sessionStorage = AuthSessionStorage(identity);
|
||||||
|
final coordinator = AuthSessionCoordinator(
|
||||||
|
sessionStorage,
|
||||||
|
preferencesStorage,
|
||||||
|
MockAssetRepository(preferencesStorage),
|
||||||
|
);
|
||||||
final controller = AuthController(
|
final controller = AuthController(
|
||||||
dio,
|
dio,
|
||||||
identityStorage: identity,
|
preferences: preferencesStorage,
|
||||||
preferences: preferences,
|
coordinator: coordinator,
|
||||||
autoBootstrap: false,
|
autoBootstrap: false,
|
||||||
);
|
);
|
||||||
fixture = _Fixture(
|
fixture = _Fixture(
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/network/api_exception.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.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_session_coordinator.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/features/assets/data/mock_asset_repository.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'concurrent unauthorized responses expire one session only once',
|
||||||
|
() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final preferences = PreferencesStorage(
|
||||||
|
await SharedPreferences.getInstance(),
|
||||||
|
);
|
||||||
|
final identity = SecureIdentityStorage(
|
||||||
|
preferences,
|
||||||
|
secureStorage: _MemorySecureStorage(),
|
||||||
|
);
|
||||||
|
final session = AuthSessionStorage(identity);
|
||||||
|
final assets = MockAssetRepository(preferences);
|
||||||
|
final coordinator = AuthSessionCoordinator(session, preferences, assets);
|
||||||
|
await coordinator.startSession('token');
|
||||||
|
final generation = coordinator.generation;
|
||||||
|
var notifications = 0;
|
||||||
|
coordinator.addListener((_) => notifications++);
|
||||||
|
final request = RequestOptions(
|
||||||
|
path: '/protected',
|
||||||
|
extra: {authSessionGenerationExtra: generation},
|
||||||
|
);
|
||||||
|
final exception = ApiException(
|
||||||
|
kind: ApiExceptionKind.backend,
|
||||||
|
httpStatus: 400,
|
||||||
|
reason: 'TOKEN_EXPIRED',
|
||||||
|
requestOptions: request,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
coordinator.expireIfNeeded(exception),
|
||||||
|
coordinator.expireIfNeeded(exception),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notifications, 1);
|
||||||
|
expect(await identity.readJwt(), isNull);
|
||||||
|
expect(await coordinator.expireIfNeeded(exception), isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('account deletion upgrades an in-flight expiration', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
storageUserId: 'user-1',
|
||||||
|
storageUserPhone: '13800138000',
|
||||||
|
storageJinzhiHoldings: 'legacy-assets',
|
||||||
|
});
|
||||||
|
final preferences = PreferencesStorage(
|
||||||
|
await SharedPreferences.getInstance(),
|
||||||
|
);
|
||||||
|
final secure = _MemorySecureStorage({
|
||||||
|
secureBiometricCredential: 'biometric',
|
||||||
|
});
|
||||||
|
final identity = SecureIdentityStorage(preferences, secureStorage: secure);
|
||||||
|
final session = AuthSessionStorage(identity);
|
||||||
|
final assets = MockAssetRepository(preferences);
|
||||||
|
final accountScope = MockAssetRepository.accountScope(userId: 'user-1');
|
||||||
|
await assets.saveHoldings(accountScope, assets.loadHoldings(accountScope));
|
||||||
|
final coordinator = AuthSessionCoordinator(session, preferences, assets);
|
||||||
|
await coordinator.startSession('token');
|
||||||
|
final notifications = <SessionEndReason>[];
|
||||||
|
coordinator.addListener(notifications.add);
|
||||||
|
|
||||||
|
final expiring = coordinator.end(SessionEndReason.expired);
|
||||||
|
final deleting = coordinator.accountDeleted();
|
||||||
|
await Future.wait([expiring, deleting]);
|
||||||
|
|
||||||
|
expect(notifications, [SessionEndReason.accountDeleted]);
|
||||||
|
expect(await secure.read(secureBiometricCredential), isNull);
|
||||||
|
expect(assets.loadHoldings(accountScope), isEmpty);
|
||||||
|
expect(preferences.containsKey(storageJinzhiHoldings), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a new login waits for an in-flight session end', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final preferences = PreferencesStorage(
|
||||||
|
await SharedPreferences.getInstance(),
|
||||||
|
);
|
||||||
|
final identity = SecureIdentityStorage(
|
||||||
|
preferences,
|
||||||
|
secureStorage: _MemorySecureStorage(),
|
||||||
|
);
|
||||||
|
final session = AuthSessionStorage(identity);
|
||||||
|
final coordinator = AuthSessionCoordinator(
|
||||||
|
session,
|
||||||
|
preferences,
|
||||||
|
MockAssetRepository(preferences),
|
||||||
|
);
|
||||||
|
final accountScope = MockAssetRepository.accountScope(userId: 'user-1');
|
||||||
|
await preferences.setString(storageUserId, 'user-1');
|
||||||
|
final assets = MockAssetRepository(preferences);
|
||||||
|
await assets.saveHoldings(accountScope, assets.loadHoldings(accountScope));
|
||||||
|
await coordinator.startSession('old-token');
|
||||||
|
|
||||||
|
final ending = coordinator.logout();
|
||||||
|
final starting = coordinator.startSession('new-token');
|
||||||
|
await Future.wait([ending, starting]);
|
||||||
|
|
||||||
|
expect(session.accessToken, 'new-token');
|
||||||
|
expect(await identity.readJwt(), 'new-token');
|
||||||
|
expect(assets.loadHoldings(accountScope), isNotEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MemorySecureStorage implements SecureKeyValueStorage {
|
||||||
|
_MemorySecureStorage([Map<String, String>? initial]) : values = {...?initial};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
|
||||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_entry_promo_store.dart';
|
import 'package:jinzhi_flutter_app/features/mall/application/mall_entry_promo_store.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('records each first-install mall promotion exposure once', () async {
|
test('records each first-install mall promotion exposure once', () async {
|
||||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||||
final store = MallEntryPromoStore(await SharedPreferences.getInstance());
|
final store = MallEntryPromoStore(
|
||||||
|
PreferencesStorage(await SharedPreferences.getInstance()),
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.hasOpenedMall, isFalse);
|
expect(store.hasOpenedMall, isFalse);
|
||||||
expect(store.hasSeenHomeBadge, isFalse);
|
expect(store.hasSeenHomeBadge, isFalse);
|
||||||
|
|||||||
Reference in new issue
Block a user