Commit 1aace1893fd2cdd5dde3d4a52503f8263632c959

Authored by 权海
1 parent 97a60633

feat(ui):添加AppleHealth原生接口和支付接口

@@ -211,6 +211,38 @@ data class HealthActivityTargetData ( @@ -211,6 +211,38 @@ data class HealthActivityTargetData (
211 211
212 override fun hashCode(): Int = toList().hashCode() 212 override fun hashCode(): Int = toList().hashCode()
213 } 213 }
  214 +
  215 +/** Generated class from Pigeon that represents data sent in messages. */
  216 +data class HealthAuthorization (
  217 + /** -1: AppleHealth不可用, 0: 需要请求授权, 1: 已被授权且能获取到部分数据, 2: 已被拒绝或者没有数据 */
  218 + val status: Long,
  219 + val hasData: Boolean
  220 +)
  221 + {
  222 + companion object {
  223 + fun fromList(pigeonVar_list: List<Any?>): HealthAuthorization {
  224 + val status = pigeonVar_list[0] as Long
  225 + val hasData = pigeonVar_list[1] as Boolean
  226 + return HealthAuthorization(status, hasData)
  227 + }
  228 + }
  229 + fun toList(): List<Any?> {
  230 + return listOf(
  231 + status,
  232 + hasData,
  233 + )
  234 + }
  235 + override fun equals(other: Any?): Boolean {
  236 + if (other !is HealthAuthorization) {
  237 + return false
  238 + }
  239 + if (this === other) {
  240 + return true
  241 + }
  242 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  243 +
  244 + override fun hashCode(): Int = toList().hashCode()
  245 +}
214 private open class HealthKitApiPigeonCodec : StandardMessageCodec() { 246 private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
215 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 247 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
216 return when (type) { 248 return when (type) {
@@ -234,6 +266,11 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -234,6 +266,11 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
234 HealthActivityTargetData.fromList(it) 266 HealthActivityTargetData.fromList(it)
235 } 267 }
236 } 268 }
  269 + 133.toByte() -> {
  270 + return (readValue(buffer) as? List<Any?>)?.let {
  271 + HealthAuthorization.fromList(it)
  272 + }
  273 + }
237 else -> super.readValueOfType(type, buffer) 274 else -> super.readValueOfType(type, buffer)
238 } 275 }
239 } 276 }
@@ -255,6 +292,10 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -255,6 +292,10 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
255 stream.write(132) 292 stream.write(132)
256 writeValue(stream, value.toList()) 293 writeValue(stream, value.toList())
257 } 294 }
  295 + is HealthAuthorization -> {
  296 + stream.write(133)
  297 + writeValue(stream, value.toList())
  298 + }
258 else -> super.writeValue(stream, value) 299 else -> super.writeValue(stream, value)
259 } 300 }
260 } 301 }
@@ -263,13 +304,13 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -263,13 +304,13 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
263 304
264 /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ 305 /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
265 interface HealthKitHostApi { 306 interface HealthKitHostApi {
266 - fun checkHealthAppAuthorization(callback: (Result<Boolean>) -> Unit)  
267 fun getHealthServerAuthUrl(callback: (Result<String>) -> Unit) 307 fun getHealthServerAuthUrl(callback: (Result<String>) -> Unit)
268 - /** Opens Huawei Health client authorization UI. Returns whether user granted. */  
269 - fun requestHealthClientAuthorization(): Boolean  
270 fun cancelHealthAppAuthorization(): Boolean 308 fun cancelHealthAppAuthorization(): Boolean
271 /** Runs native health read and server upload pipeline. */ 309 /** Runs native health read and server upload pipeline. */
272 fun performHealthUpload(): HealthUploadResult 310 fun performHealthUpload(): HealthUploadResult
  311 + /** Opens Huawei Health client authorization UI. Returns whether user granted. */
  312 + fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
  313 + fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
273 fun fetchHrvData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit) 314 fun fetchHrvData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit)
274 fun fetchHeartRateData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit) 315 fun fetchHeartRateData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit)
275 fun fetchWalkingHeartRateData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit) 316 fun fetchWalkingHeartRateData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit)
@@ -296,10 +337,10 @@ interface HealthKitHostApi { @@ -296,10 +337,10 @@ interface HealthKitHostApi {
296 fun setUp(binaryMessenger: BinaryMessenger, api: HealthKitHostApi?, messageChannelSuffix: String = "") { 337 fun setUp(binaryMessenger: BinaryMessenger, api: HealthKitHostApi?, messageChannelSuffix: String = "") {
297 val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" 338 val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
298 run { 339 run {
299 - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$separatedMessageChannelSuffix", codec) 340 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$separatedMessageChannelSuffix", codec)
300 if (api != null) { 341 if (api != null) {
301 channel.setMessageHandler { _, reply -> 342 channel.setMessageHandler { _, reply ->
302 - api.checkHealthAppAuthorization{ result: Result<Boolean> -> 343 + api.getHealthServerAuthUrl{ result: Result<String> ->
303 val error = result.exceptionOrNull() 344 val error = result.exceptionOrNull()
304 if (error != null) { 345 if (error != null) {
305 reply.reply(HealthKitApiPigeonUtils.wrapError(error)) 346 reply.reply(HealthKitApiPigeonUtils.wrapError(error))
@@ -314,29 +355,26 @@ interface HealthKitHostApi { @@ -314,29 +355,26 @@ interface HealthKitHostApi {
314 } 355 }
315 } 356 }
316 run { 357 run {
317 - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$separatedMessageChannelSuffix", codec) 358 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$separatedMessageChannelSuffix", codec)
318 if (api != null) { 359 if (api != null) {
319 channel.setMessageHandler { _, reply -> 360 channel.setMessageHandler { _, reply ->
320 - api.getHealthServerAuthUrl{ result: Result<String> ->  
321 - val error = result.exceptionOrNull()  
322 - if (error != null) {  
323 - reply.reply(HealthKitApiPigeonUtils.wrapError(error))  
324 - } else {  
325 - val data = result.getOrNull()  
326 - reply.reply(HealthKitApiPigeonUtils.wrapResult(data))  
327 - } 361 + val wrapped: List<Any?> = try {
  362 + listOf(api.cancelHealthAppAuthorization())
  363 + } catch (exception: Throwable) {
  364 + HealthKitApiPigeonUtils.wrapError(exception)
328 } 365 }
  366 + reply.reply(wrapped)
329 } 367 }
330 } else { 368 } else {
331 channel.setMessageHandler(null) 369 channel.setMessageHandler(null)
332 } 370 }
333 } 371 }
334 run { 372 run {
335 - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$separatedMessageChannelSuffix", codec) 373 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$separatedMessageChannelSuffix", codec)
336 if (api != null) { 374 if (api != null) {
337 channel.setMessageHandler { _, reply -> 375 channel.setMessageHandler { _, reply ->
338 val wrapped: List<Any?> = try { 376 val wrapped: List<Any?> = try {
339 - listOf(api.requestHealthClientAuthorization()) 377 + listOf(api.performHealthUpload())
340 } catch (exception: Throwable) { 378 } catch (exception: Throwable) {
341 HealthKitApiPigeonUtils.wrapError(exception) 379 HealthKitApiPigeonUtils.wrapError(exception)
342 } 380 }
@@ -347,30 +385,36 @@ interface HealthKitHostApi { @@ -347,30 +385,36 @@ interface HealthKitHostApi {
347 } 385 }
348 } 386 }
349 run { 387 run {
350 - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$separatedMessageChannelSuffix", codec) 388 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$separatedMessageChannelSuffix", codec)
351 if (api != null) { 389 if (api != null) {
352 channel.setMessageHandler { _, reply -> 390 channel.setMessageHandler { _, reply ->
353 - val wrapped: List<Any?> = try {  
354 - listOf(api.cancelHealthAppAuthorization())  
355 - } catch (exception: Throwable) {  
356 - HealthKitApiPigeonUtils.wrapError(exception) 391 + api.checkHealthAppAuthorization{ result: Result<HealthAuthorization> ->
  392 + val error = result.exceptionOrNull()
  393 + if (error != null) {
  394 + reply.reply(HealthKitApiPigeonUtils.wrapError(error))
  395 + } else {
  396 + val data = result.getOrNull()
  397 + reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
  398 + }
357 } 399 }
358 - reply.reply(wrapped)  
359 } 400 }
360 } else { 401 } else {
361 channel.setMessageHandler(null) 402 channel.setMessageHandler(null)
362 } 403 }
363 } 404 }
364 run { 405 run {
365 - val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$separatedMessageChannelSuffix", codec) 406 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$separatedMessageChannelSuffix", codec)
366 if (api != null) { 407 if (api != null) {
367 channel.setMessageHandler { _, reply -> 408 channel.setMessageHandler { _, reply ->
368 - val wrapped: List<Any?> = try {  
369 - listOf(api.performHealthUpload())  
370 - } catch (exception: Throwable) {  
371 - HealthKitApiPigeonUtils.wrapError(exception) 409 + api.requestHealthClientAuthorization{ result: Result<Boolean> ->
  410 + val error = result.exceptionOrNull()
  411 + if (error != null) {
  412 + reply.reply(HealthKitApiPigeonUtils.wrapError(error))
  413 + } else {
  414 + val data = result.getOrNull()
  415 + reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
  416 + }
372 } 417 }
373 - reply.reply(wrapped)  
374 } 418 }
375 } else { 419 } else {
376 channel.setMessageHandler(null) 420 channel.setMessageHandler(null)
@@ -79,6 +79,20 @@ class FlutterError ( @@ -79,6 +79,20 @@ class FlutterError (
79 val details: Any? = null 79 val details: Any? = null
80 ) : Throwable() 80 ) : Throwable()
81 81
  82 +enum class AppleProductPaymentErrorMsg(val raw: Int) {
  83 + MISSING_UUID(0),
  84 + PRODUCT_NOT_FOUND(1),
  85 + USER_CANCELLED(2),
  86 + FAILED_VERIFICATION(3),
  87 + UNKNOWN(4);
  88 +
  89 + companion object {
  90 + fun ofRaw(raw: Int): AppleProductPaymentErrorMsg? {
  91 + return values().firstOrNull { it.raw == raw }
  92 + }
  93 + }
  94 +}
  95 +
82 /** Generated class from Pigeon that represents data sent in messages. */ 96 /** Generated class from Pigeon that represents data sent in messages. */
83 data class AppleSignInModel ( 97 data class AppleSignInModel (
84 val userId: String, 98 val userId: String,
@@ -115,21 +129,139 @@ data class AppleSignInModel ( @@ -115,21 +129,139 @@ data class AppleSignInModel (
115 129
116 override fun hashCode(): Int = toList().hashCode() 130 override fun hashCode(): Int = toList().hashCode()
117 } 131 }
  132 +
  133 +/** Generated class from Pigeon that represents data sent in messages. */
  134 +data class AppleProductInfo (
  135 + val productId: String,
  136 + val originPriceDescription: String,
  137 + val priceDescription: String? = null,
  138 + val originPrice: Double,
  139 + val price: Double? = null,
  140 + val isTrialPeriod: Boolean
  141 +)
  142 + {
  143 + companion object {
  144 + fun fromList(pigeonVar_list: List<Any?>): AppleProductInfo {
  145 + val productId = pigeonVar_list[0] as String
  146 + val originPriceDescription = pigeonVar_list[1] as String
  147 + val priceDescription = pigeonVar_list[2] as String?
  148 + val originPrice = pigeonVar_list[3] as Double
  149 + val price = pigeonVar_list[4] as Double?
  150 + val isTrialPeriod = pigeonVar_list[5] as Boolean
  151 + return AppleProductInfo(productId, originPriceDescription, priceDescription, originPrice, price, isTrialPeriod)
  152 + }
  153 + }
  154 + fun toList(): List<Any?> {
  155 + return listOf(
  156 + productId,
  157 + originPriceDescription,
  158 + priceDescription,
  159 + originPrice,
  160 + price,
  161 + isTrialPeriod,
  162 + )
  163 + }
  164 + override fun equals(other: Any?): Boolean {
  165 + if (other !is AppleProductInfo) {
  166 + return false
  167 + }
  168 + if (this === other) {
  169 + return true
  170 + }
  171 + return PlatformApiPigeonUtils.deepEquals(toList(), other.toList()) }
  172 +
  173 + override fun hashCode(): Int = toList().hashCode()
  174 +}
  175 +
  176 +/** Generated class from Pigeon that represents data sent in messages. */
  177 +data class AppleProductPaymentResult (
  178 + val productId: String,
  179 + val appAccountToken: String? = null,
  180 + val originalTransactionId: String? = null,
  181 + val transactionId: String? = null,
  182 + val success: Boolean? = null,
  183 + /**
  184 + * 错误描述:
  185 + * 和AppleProductPaymentErrorMsg匹配的flutter处理,
  186 + * 不匹配的则是StoreKit 直接给的错误描述,直接展示即可
  187 + */
  188 + val errorMessage: String? = null
  189 +)
  190 + {
  191 + companion object {
  192 + fun fromList(pigeonVar_list: List<Any?>): AppleProductPaymentResult {
  193 + val productId = pigeonVar_list[0] as String
  194 + val appAccountToken = pigeonVar_list[1] as String?
  195 + val originalTransactionId = pigeonVar_list[2] as String?
  196 + val transactionId = pigeonVar_list[3] as String?
  197 + val success = pigeonVar_list[4] as Boolean?
  198 + val errorMessage = pigeonVar_list[5] as String?
  199 + return AppleProductPaymentResult(productId, appAccountToken, originalTransactionId, transactionId, success, errorMessage)
  200 + }
  201 + }
  202 + fun toList(): List<Any?> {
  203 + return listOf(
  204 + productId,
  205 + appAccountToken,
  206 + originalTransactionId,
  207 + transactionId,
  208 + success,
  209 + errorMessage,
  210 + )
  211 + }
  212 + override fun equals(other: Any?): Boolean {
  213 + if (other !is AppleProductPaymentResult) {
  214 + return false
  215 + }
  216 + if (this === other) {
  217 + return true
  218 + }
  219 + return PlatformApiPigeonUtils.deepEquals(toList(), other.toList()) }
  220 +
  221 + override fun hashCode(): Int = toList().hashCode()
  222 +}
118 private open class PlatformApiPigeonCodec : StandardMessageCodec() { 223 private open class PlatformApiPigeonCodec : StandardMessageCodec() {
119 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 224 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
120 return when (type) { 225 return when (type) {
121 129.toByte() -> { 226 129.toByte() -> {
  227 + return (readValue(buffer) as Long?)?.let {
  228 + AppleProductPaymentErrorMsg.ofRaw(it.toInt())
  229 + }
  230 + }
  231 + 130.toByte() -> {
122 return (readValue(buffer) as? List<Any?>)?.let { 232 return (readValue(buffer) as? List<Any?>)?.let {
123 AppleSignInModel.fromList(it) 233 AppleSignInModel.fromList(it)
124 } 234 }
125 } 235 }
  236 + 131.toByte() -> {
  237 + return (readValue(buffer) as? List<Any?>)?.let {
  238 + AppleProductInfo.fromList(it)
  239 + }
  240 + }
  241 + 132.toByte() -> {
  242 + return (readValue(buffer) as? List<Any?>)?.let {
  243 + AppleProductPaymentResult.fromList(it)
  244 + }
  245 + }
126 else -> super.readValueOfType(type, buffer) 246 else -> super.readValueOfType(type, buffer)
127 } 247 }
128 } 248 }
129 override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { 249 override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
130 when (value) { 250 when (value) {
131 - is AppleSignInModel -> { 251 + is AppleProductPaymentErrorMsg -> {
132 stream.write(129) 252 stream.write(129)
  253 + writeValue(stream, value.raw)
  254 + }
  255 + is AppleSignInModel -> {
  256 + stream.write(130)
  257 + writeValue(stream, value.toList())
  258 + }
  259 + is AppleProductInfo -> {
  260 + stream.write(131)
  261 + writeValue(stream, value.toList())
  262 + }
  263 + is AppleProductPaymentResult -> {
  264 + stream.write(132)
133 writeValue(stream, value.toList()) 265 writeValue(stream, value.toList())
134 } 266 }
135 else -> super.writeValue(stream, value) 267 else -> super.writeValue(stream, value)
@@ -145,7 +277,12 @@ interface PlatformHostApi { @@ -145,7 +277,12 @@ interface PlatformHostApi {
145 * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 277 * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
146 */ 278 */
147 fun getFullUserAgent(): String 279 fun getFullUserAgent(): String
  280 + /** 请求苹果登录 */
148 fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit) 281 fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit)
  282 + /** 查询指定id的苹果商品 */
  283 + fun requestAppleProductInfo(productId: String, callback: (Result<AppleProductInfo?>) -> Unit)
  284 + /** 从服务器下单后请求苹果支付 */
  285 + fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit)
149 286
150 companion object { 287 companion object {
151 /** The codec used by PlatformHostApi. */ 288 /** The codec used by PlatformHostApi. */
@@ -189,6 +326,47 @@ interface PlatformHostApi { @@ -189,6 +326,47 @@ interface PlatformHostApi {
189 channel.setMessageHandler(null) 326 channel.setMessageHandler(null)
190 } 327 }
191 } 328 }
  329 + run {
  330 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleProductInfo$separatedMessageChannelSuffix", codec)
  331 + if (api != null) {
  332 + channel.setMessageHandler { message, reply ->
  333 + val args = message as List<Any?>
  334 + val productIdArg = args[0] as String
  335 + api.requestAppleProductInfo(productIdArg) { result: Result<AppleProductInfo?> ->
  336 + val error = result.exceptionOrNull()
  337 + if (error != null) {
  338 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  339 + } else {
  340 + val data = result.getOrNull()
  341 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  342 + }
  343 + }
  344 + }
  345 + } else {
  346 + channel.setMessageHandler(null)
  347 + }
  348 + }
  349 + run {
  350 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performApplePayment$separatedMessageChannelSuffix", codec)
  351 + if (api != null) {
  352 + channel.setMessageHandler { message, reply ->
  353 + val args = message as List<Any?>
  354 + val productIdArg = args[0] as String
  355 + val uuidArg = args[1] as String
  356 + api.performApplePayment(productIdArg, uuidArg) { result: Result<AppleProductPaymentResult?> ->
  357 + val error = result.exceptionOrNull()
  358 + if (error != null) {
  359 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  360 + } else {
  361 + val data = result.getOrNull()
  362 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  363 + }
  364 + }
  365 + }
  366 + } else {
  367 + channel.setMessageHandler(null)
  368 + }
  369 + }
192 } 370 }
193 } 371 }
194 } 372 }
@@ -745,6 +745,7 @@ @@ -745,6 +745,7 @@
745 "$(PODS_CONFIGURATION_BUILD_DIR)", 745 "$(PODS_CONFIGURATION_BUILD_DIR)",
746 "$(FLUTTER_ROOT)/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator", 746 "$(FLUTTER_ROOT)/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator",
747 ); 747 );
  748 + GCC_OPTIMIZATION_LEVEL = s;
748 GENERATE_INFOPLIST_FILE = YES; 749 GENERATE_INFOPLIST_FILE = YES;
749 "HEADER_SEARCH_PATHS[sdk=iphoneos*]" = ( 750 "HEADER_SEARCH_PATHS[sdk=iphoneos*]" = (
750 "$(inherited)", 751 "$(inherited)",
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 shouldAutocreateTestPlan = "YES"> 32 shouldAutocreateTestPlan = "YES">
33 </TestAction> 33 </TestAction>
34 <LaunchAction 34 <LaunchAction
35 - buildConfiguration = "Debug" 35 + buildConfiguration = "Release"
36 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 36 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
37 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 37 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
38 customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" 38 customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
@@ -12,16 +12,26 @@ import UserNotifications @@ -12,16 +12,26 @@ import UserNotifications
12 import AppTrackingTransparency 12 import AppTrackingTransparency
13 import AdSupport 13 import AdSupport
14 14
  15 +typealias FlutterBridgeMethod = ((_ params: Any?, _ result: FlutterResult) -> Void)
  16 +
  17 +
15 @Observable 18 @Observable
16 class AppDelegate: NSObject, UIApplicationDelegate { 19 class AppDelegate: NSObject, UIApplicationDelegate {
17 20
  21 + enum FlutterBridgeMethodName: String{
  22 + case verifyPayment
  23 + }
  24 + private var methods: [String: FlutterBridgeMethod] = [:]
  25 +
18 let flutterEngine: FlutterEngine = FlutterEngine(name: "main_flutter_engine") 26 let flutterEngine: FlutterEngine = FlutterEngine(name: "main_flutter_engine")
  27 + var channel: FlutterMethodChannel?
19 28
20 func application( 29 func application(
21 _ application: UIApplication, 30 _ application: UIApplication,
22 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 31 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
23 ) -> Bool { 32 ) -> Bool {
24 UserAgent.share.genUA() 33 UserAgent.share.genUA()
  34 + ApplePayment.shared.delegate = self
25 35
26 UIApplication.shared.registerForRemoteNotifications() 36 UIApplication.shared.registerForRemoteNotifications()
27 UNUserNotificationCenter.current().setBadgeCount(0) 37 UNUserNotificationCenter.current().setBadgeCount(0)
@@ -31,9 +41,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { @@ -31,9 +41,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
31 flutterEngine.run() 41 flutterEngine.run()
32 GeneratedPluginRegistrant.register(with: flutterEngine) 42 GeneratedPluginRegistrant.register(with: flutterEngine)
33 NativePigeonRegistrar.register(binaryMessenger: flutterEngine.binaryMessenger) 43 NativePigeonRegistrar.register(binaryMessenger: flutterEngine.binaryMessenger)
  44 + channel = FlutterMethodChannel(name: "doublefeel_flutter_main_channel", binaryMessenger: flutterEngine.binaryMessenger)
  45 + methods = defaultMethods()
  46 +
34 WatchConnectivityService.shared.activate() 47 WatchConnectivityService.shared.activate()
35 HealthKitService.shared.startBackgroundObserversIfNeeded() 48 HealthKitService.shared.startBackgroundObserversIfNeeded()
36 -//  
37 return true 49 return true
38 } 50 }
39 51
@@ -51,6 +63,21 @@ class AppDelegate: NSObject, UIApplicationDelegate { @@ -51,6 +63,21 @@ class AppDelegate: NSObject, UIApplicationDelegate {
51 } 63 }
52 } 64 }
53 65
  66 +extension AppDelegate{
  67 + private func defaultMethods() -> [String: FlutterBridgeMethod]{
  68 + var methods: [String: FlutterBridgeMethod] = [:]
  69 + methods[FlutterBridgeMethodName.verifyPayment.rawValue] = { params, result in
  70 + result(params)
  71 + }
  72 +
  73 + return methods
  74 + }
  75 +
  76 + func invoke(method: FlutterBridgeMethodName, arguments: Any?, result: @escaping FlutterResult){
  77 + channel?.invokeMethod(method.rawValue, arguments: arguments, result: result)
  78 + }
  79 +}
  80 +
54 extension AppDelegate: UNUserNotificationCenterDelegate{ 81 extension AppDelegate: UNUserNotificationCenterDelegate{
55 func userNotificationCenter(_ center: UNUserNotificationCenter, 82 func userNotificationCenter(_ center: UNUserNotificationCenter,
56 willPresent notification: UNNotification, 83 willPresent notification: UNNotification,
@@ -118,39 +118,39 @@ struct AppleHealthTestView: View { @@ -118,39 +118,39 @@ struct AppleHealthTestView: View {
118 118
119 private func runPermissionCheck() async { 119 private func runPermissionCheck() async {
120 do { 120 do {
121 - let isAuthorized = try await checkHealthAuthorization()  
122 - await appendLog("checkHealthAppAuthorization = \(isAuthorized)") 121 + let authorization = try await checkHealthAuthorization()
  122 + appendLog("checkHealthAppAuthorization status=\(authorization.status), hasData=\(authorization.hasData)")
123 123
124 let authUrl = try await getHealthServerAuthUrl() 124 let authUrl = try await getHealthServerAuthUrl()
125 - await appendLog("getHealthServerAuthUrl = \(authUrl.isEmpty ? "<empty, iOS system managed>" : authUrl)") 125 + appendLog("getHealthServerAuthUrl = \(authUrl.isEmpty ? "<empty, iOS system managed>" : authUrl)")
126 126
127 - if !isAuthorized {  
128 - await appendLog("当前未授权,开始 requestHealthClientAuthorization")  
129 - let granted = try api.requestHealthClientAuthorization()  
130 - await appendLog("requestHealthClientAuthorization = \(granted)") 127 + if authorization.status == 0 {
  128 + appendLog("当前需要请求授权,开始 requestHealthClientAuthorization")
  129 + let granted = try await requestHealthClientAuthorization()
  130 + appendLog("requestHealthClientAuthorization = \(granted)")
131 } else { 131 } else {
132 - await appendLog("当前已授权,跳过 requestHealthClientAuthorization") 132 + appendLog("当前不需要再次请求授权,跳过 requestHealthClientAuthorization")
133 } 133 }
134 134
135 let cancelResult = try api.cancelHealthAppAuthorization() 135 let cancelResult = try api.cancelHealthAppAuthorization()
136 - await appendLog("cancelHealthAppAuthorization = \(cancelResult) (iOS 不支持应用内撤销)") 136 + appendLog("cancelHealthAppAuthorization = \(cancelResult) (iOS 不支持应用内撤销)")
137 } catch { 137 } catch {
138 - await appendError("权限检查失败", error) 138 + appendError("权限检查失败", error)
139 } 139 }
140 } 140 }
141 141
142 private func runDataFetch() async { 142 private func runDataFetch() async {
143 let endTime = Int64(Date().timeIntervalSince1970) 143 let endTime = Int64(Date().timeIntervalSince1970)
144 let startTime = Int64(Calendar.current.date(byAdding: .day, value: -7, to: Date())?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) 144 let startTime = Int64(Calendar.current.date(byAdding: .day, value: -7, to: Date())?.timeIntervalSince1970 ?? Date().timeIntervalSince1970)
145 - await appendLog("数据范围: \(formatTimestamp(startTime)) -> \(formatTimestamp(endTime))") 145 + appendLog("数据范围: \(formatTimestamp(startTime)) -> \(formatTimestamp(endTime))")
146 146
147 do { 147 do {
148 let uploadResult = try api.performHealthUpload() 148 let uploadResult = try api.performHealthUpload()
149 - await appendLog( 149 + appendLog(
150 "performHealthUpload common=\(uploadResult.commonUploadSuccess), sleep=\(uploadResult.sleepUploadSuccess), error=\(uploadResult.errorMessage ?? "nil")" 150 "performHealthUpload common=\(uploadResult.commonUploadSuccess), sleep=\(uploadResult.sleepUploadSuccess), error=\(uploadResult.errorMessage ?? "nil")"
151 ) 151 )
152 } catch { 152 } catch {
153 - await appendError("performHealthUpload 失败", error) 153 + appendError("performHealthUpload 失败", error)
154 } 154 }
155 155
156 await fetchCommon("HRV", startTime, endTime, api.fetchHrvData) 156 await fetchCommon("HRV", startTime, endTime, api.fetchHrvData)
@@ -182,10 +182,10 @@ struct AppleHealthTestView: View { @@ -182,10 +182,10 @@ struct AppleHealthTestView: View {
182 continuation.resume(with: result) 182 continuation.resume(with: result)
183 } 183 }
184 } 184 }
185 - await appendLog("\(title): \(points.count) 条")  
186 - await appendSample(points) 185 + appendLog("\(title): \(points.count) 条")
  186 + appendSample(points)
187 } catch { 187 } catch {
188 - await appendError("\(title) 获取失败", error) 188 + appendError("\(title) 获取失败", error)
189 } 189 }
190 } 190 }
191 191
@@ -196,12 +196,12 @@ struct AppleHealthTestView: View { @@ -196,12 +196,12 @@ struct AppleHealthTestView: View {
196 continuation.resume(with: result) 196 continuation.resume(with: result)
197 } 197 }
198 } 198 }
199 - await appendLog("睡眠: \(points.count) 条") 199 + appendLog("睡眠: \(points.count) 条")
200 for point in points.prefix(3) { 200 for point in points.prefix(3) {
201 - await appendLog(" sample dataType=\(point.dataType), from=\(formatTimestamp(point.fromTime)), to=\(formatTimestamp(point.toTime))") 201 + appendLog(" sample dataType=\(point.dataType), from=\(formatTimestamp(point.fromTime)), to=\(formatTimestamp(point.toTime))")
202 } 202 }
203 } catch { 203 } catch {
204 - await appendError("睡眠获取失败", error) 204 + appendError("睡眠获取失败", error)
205 } 205 }
206 } 206 }
207 207
@@ -213,16 +213,16 @@ struct AppleHealthTestView: View { @@ -213,16 +213,16 @@ struct AppleHealthTestView: View {
213 } 213 }
214 } 214 }
215 if let target { 215 if let target {
216 - await appendLog("活动目标: move=\(target.move.map(String.init) ?? "nil"), stand=\(target.stand.map(String.init) ?? "nil")") 216 + appendLog("活动目标: move=\(target.move.map(String.init) ?? "nil"), stand=\(target.stand.map(String.init) ?? "nil")")
217 } else { 217 } else {
218 - await appendLog("活动目标: nil") 218 + appendLog("活动目标: nil")
219 } 219 }
220 } catch { 220 } catch {
221 - await appendError("活动目标获取失败", error) 221 + appendError("活动目标获取失败", error)
222 } 222 }
223 } 223 }
224 224
225 - private func checkHealthAuthorization() async throws -> Bool { 225 + private func checkHealthAuthorization() async throws -> HealthAuthorization {
226 try await withCheckedThrowingContinuation { continuation in 226 try await withCheckedThrowingContinuation { continuation in
227 api.checkHealthAppAuthorization { result in 227 api.checkHealthAppAuthorization { result in
228 continuation.resume(with: result) 228 continuation.resume(with: result)
@@ -230,6 +230,14 @@ struct AppleHealthTestView: View { @@ -230,6 +230,14 @@ struct AppleHealthTestView: View {
230 } 230 }
231 } 231 }
232 232
  233 + private func requestHealthClientAuthorization() async throws -> Bool {
  234 + try await withCheckedThrowingContinuation { continuation in
  235 + api.requestHealthClientAuthorization { result in
  236 + continuation.resume(with: result)
  237 + }
  238 + }
  239 + }
  240 +
233 private func getHealthServerAuthUrl() async throws -> String { 241 private func getHealthServerAuthUrl() async throws -> String {
234 try await withCheckedThrowingContinuation { continuation in 242 try await withCheckedThrowingContinuation { continuation in
235 api.getHealthServerAuthUrl { result in 243 api.getHealthServerAuthUrl { result in
  1 +//
  2 +// ApplePayment.swift
  3 +// hippo
  4 +//
  5 +// Created by shihao on 2025/6/27.
  6 +//
  7 +
  8 +import Foundation
  9 +import StoreKit
  10 +
  11 +enum StoreKitError: Error, LocalizedError {
  12 + case productNotFound
  13 + case failedVerification
  14 + case unknown
  15 +
  16 + var errorDescription: String? {
  17 + switch self {
  18 + case .productNotFound:
  19 + return String(describing: AppleProductPaymentErrorMsg.productNotFound)
  20 + case .failedVerification:
  21 + return String(describing: AppleProductPaymentErrorMsg.failedVerification)
  22 + case .unknown:
  23 + return String(describing: AppleProductPaymentErrorMsg.unknown)
  24 + }
  25 + }
  26 +}
  27 +
  28 +class ApplePayment {
  29 + static let shared = ApplePayment()
  30 + var delegate: AppDelegate?
  31 +
  32 + func purchase(_ productId: String, uuidString: String) async -> AppleProductPaymentResult {
  33 + do {
  34 + guard let uuid = UUID(uuidString: uuidString) else {
  35 + return paymentResult(
  36 + productId: productId,
  37 + success: false,
  38 + errorMessage: String(describing: AppleProductPaymentErrorMsg.missingUUID)
  39 + )
  40 + }
  41 + let appleProduct = try await requestProducts(productId)
  42 + let result = try await appleProduct.purchase(options: [
  43 + Product.PurchaseOption.appAccountToken(uuid)
  44 + ])
  45 +
  46 + switch result {
  47 + case .success(let verification):
  48 + let transaction = try checkVerified(verification)
  49 +
  50 + let success = await verifyWithServer(appAccountToken: transaction.appAccountToken?.uuidString ?? "", originalTransactionId: String(transaction.originalID), transactionId: String(transaction.id), productId: transaction.productID)
  51 + // 更新客户产品状态
  52 + await updateCustomerProductStatus()
  53 + // 完成交易
  54 + if success{
  55 + await transaction.finish()
  56 + return paymentResult(
  57 + productId: productId,
  58 + appAccountToken: transaction.appAccountToken?.uuidString,
  59 + originalTransactionId: String(transaction.originalID),
  60 + transactionId: String(transaction.id),
  61 + success: true
  62 + )
  63 + }else{
  64 + return paymentResult(
  65 + productId: productId,
  66 + appAccountToken: transaction.appAccountToken?.uuidString,
  67 + originalTransactionId: String(transaction.originalID),
  68 + transactionId: String(transaction.id),
  69 + success: false,
  70 + errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
  71 + )
  72 + }
  73 + case .userCancelled:
  74 + return paymentResult(
  75 + productId: productId,
  76 + success: false,
  77 + errorMessage: String(describing: AppleProductPaymentErrorMsg.userCancelled)
  78 + )
  79 + case .pending:
  80 + return paymentResult(
  81 + productId: productId,
  82 + success: false,
  83 + errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
  84 + )
  85 + @unknown default:
  86 + return paymentResult(
  87 + productId: productId,
  88 + success: false,
  89 + errorMessage: String(describing: AppleProductPaymentErrorMsg.unknown)
  90 + )
  91 + }
  92 + } catch {
  93 + return paymentResult(
  94 + productId: productId,
  95 + success: false,
  96 + errorMessage: paymentErrorMessage(error)
  97 + )
  98 + }
  99 +
  100 + }
  101 +
  102 + //获取苹果商品
  103 + func requestProducts(_ productAppleId: String) async throws
  104 + -> Product
  105 + {
  106 + let storeProducts = try await Product.products(
  107 + for: Set([productAppleId])
  108 + )
  109 + if let product = storeProducts.first {
  110 + return product
  111 + } else {
  112 + throw StoreKitError.productNotFound
  113 + }
  114 + }
  115 +
  116 + func isFreeTrail(product: Product) async -> Bool{
  117 + let isActive = await hasActiveEntitlement(productID: product.id)
  118 + let eligible = await product.subscription?.isEligibleForIntroOffer ?? false
  119 +
  120 + return !isActive && eligible
  121 + }
  122 +
  123 + func hasActiveEntitlement(productID: String) async -> Bool {
  124 + for await result in Transaction.currentEntitlements {
  125 + guard case .verified(let transaction) = result else {
  126 + continue
  127 + }
  128 + if transaction.productID == productID,
  129 + transaction.revocationDate == nil {
  130 + return true
  131 + }
  132 + }
  133 + return false
  134 + }
  135 +
  136 + private func updateCustomerProductStatus() async {
  137 + var activeSubscriptions: [String] = []
  138 +
  139 + for await result in Transaction.currentEntitlements {
  140 + do {
  141 + let transaction = try checkVerified(result)
  142 +
  143 + switch transaction.productType {
  144 + case .autoRenewable:
  145 + if let expirationDate = transaction.expirationDate,
  146 + expirationDate > Date()
  147 + {
  148 + activeSubscriptions.append(transaction.productID)
  149 + }
  150 + default:
  151 + break
  152 + }
  153 + } catch {
  154 + print("验证交易失败: \(error)")
  155 + }
  156 + }
  157 +
  158 + }
  159 +
  160 + // MARK: - 验证交易
  161 + private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
  162 + switch result {
  163 + case .unverified:
  164 + throw StoreKitError.failedVerification
  165 + case .verified(let safe):
  166 + return safe
  167 + }
  168 + }
  169 +
  170 + private func verifyWithServer(appAccountToken: String, originalTransactionId: String?, transactionId: String?, productId: String) async -> Bool{
  171 + guard let delegate else{
  172 + return false
  173 + }
  174 + var params: [String: String] = [
  175 + "productId": productId,
  176 + "appAccountToken": appAccountToken
  177 + ]
  178 + params["originalTransactionId"] = originalTransactionId
  179 + params["transactionId"] = transactionId
  180 +
  181 + return await withCheckedContinuation { continuous in
  182 + delegate.invoke(method: .verifyPayment, arguments: params) { result in
  183 + print("invoke verifyPayment result: \(String(describing: result))")
  184 + continuous.resume(returning: true)
  185 + }
  186 + }
  187 + }
  188 +
  189 + private func paymentResult(
  190 + productId: String,
  191 + appAccountToken: String? = nil,
  192 + originalTransactionId: String? = nil,
  193 + transactionId: String? = nil,
  194 + success: Bool?,
  195 + errorMessage: String? = nil
  196 + ) -> AppleProductPaymentResult {
  197 + AppleProductPaymentResult(
  198 + productId: productId,
  199 + appAccountToken: appAccountToken,
  200 + originalTransactionId: originalTransactionId,
  201 + transactionId: transactionId,
  202 + success: success,
  203 + errorMessage: errorMessage
  204 + )
  205 + }
  206 +
  207 + private func paymentErrorMessage(_ error: Error) -> String {
  208 + if let storeKitError = error as? StoreKitError {
  209 + return storeKitError.localizedDescription
  210 + }
  211 + return error.localizedDescription
  212 + }
  213 +}
  214 +
  215 +extension ApplePayment {
  216 + // MARK: - 监听交易更新
  217 + func listenForTransactions() -> Task<Void, Error> {
  218 + return Task.detached {
  219 + for await result in Transaction.updates {
  220 + do {
  221 + let transaction = try await self.checkVerified(result)
  222 + let _ = await self.verifyWithServer(appAccountToken: transaction.appAccountToken?.uuidString ?? "", originalTransactionId: String(transaction.originalID), transactionId: String(transaction.id), productId: transaction.productID)
  223 + // 更新客户产品状态
  224 + await self.updateCustomerProductStatus()
  225 + await transaction.finish()
  226 + print("交易更新处理完成: \(transaction.debugDescription)")
  227 + } catch {
  228 + print("交易更新处理失败: \(error)")
  229 + }
  230 + }
  231 + }
  232 + }
  233 +}
  1 +//
  2 +// DebugLogger.swift
  3 +// Runner
  4 +//
  5 +// Created by 权海 on 2026/6/16.
  6 +//
  7 +
  8 +import Foundation
  9 +//import os
  10 +
  11 +
  12 +class DebugLogger{
  13 +// let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "App", category: "HealthKit")
  14 +
  15 + static func log(desc: String){
  16 + print(desc)
  17 + }
  18 +}
@@ -36,16 +36,67 @@ final class HealthKitService { @@ -36,16 +36,67 @@ final class HealthKitService {
36 } 36 }
37 } 37 }
38 38
39 - func shouldRequestAuthorization() async -> Bool {  
40 - guard isHealthDataAvailable else { return false } 39 + func authorizationRequestStatus() async -> HKAuthorizationRequestStatus? {
41 do { 40 do {
42 - let status = try await healthStore.statusForAuthorizationRequest( 41 + return try await healthStore.statusForAuthorizationRequest(
43 toShare: NativeHealthTypeCatalog.writeTypes, 42 toShare: NativeHealthTypeCatalog.writeTypes,
44 read: NativeHealthTypeCatalog.readTypes 43 read: NativeHealthTypeCatalog.readTypes
45 ) 44 )
46 - return status == .shouldRequest  
47 } catch { 45 } catch {
48 - return true 46 + return nil
  47 + }
  48 + }
  49 +
  50 + func shouldRequestAuthorization() async -> Bool {
  51 + guard isHealthDataAvailable else { return false }
  52 + return await authorizationRequestStatus() != .unnecessary
  53 + }
  54 +
  55 + func hasAnyReadableData() async -> Bool {
  56 + guard isHealthDataAvailable else { return false }
  57 +
  58 + let endDate = Date()
  59 + let startDate = Calendar.current.date(byAdding: .year, value: -2, to: endDate)
  60 + ?? Date(timeInterval: -2 * 365 * 24 * 60 * 60, since: endDate)
  61 +
  62 + let commonFetches: [(Date, Date) async throws -> [NativeHealthDataPoint]] = [
  63 + reader.fetchHrvData,
  64 + reader.fetchHeartRateData,
  65 + reader.fetchWalkingHeartRateData,
  66 + reader.fetchRestingHeartRateData,
  67 + reader.fetchSleepingHeartRateData,
  68 + reader.fetchOxygenSaturationData,
  69 + reader.fetchActiveEnergyData,
  70 + reader.fetchExerciseData,
  71 + reader.fetchStandData,
  72 + reader.fetchStepCountData,
  73 + reader.fetchSleepingWristTemperatureData,
  74 + reader.fetchRespiratoryRateData,
  75 + reader.fetchIrregularHeartRhythmData,
  76 + ]
  77 +
  78 + for fetch in commonFetches {
  79 + do {
  80 + if try await !fetch(startDate, endDate).isEmpty {
  81 + return true
  82 + }
  83 + } catch {
  84 + // Keep probing other data types; HealthKit may deny or lack a single type.
  85 + }
  86 + }
  87 +
  88 + do {
  89 + if try await !reader.fetchSleepData(startDate: startDate, endDate: endDate).isEmpty {
  90 + return true
  91 + }
  92 + } catch {
  93 + // Keep probing activity summaries.
  94 + }
  95 +
  96 + do {
  97 + return try await reader.fetchActivityTargetData(startDate: startDate, endDate: endDate) != nil
  98 + } catch {
  99 + return false
49 } 100 }
50 } 101 }
51 102
@@ -240,6 +240,36 @@ struct HealthActivityTargetData: Hashable { @@ -240,6 +240,36 @@ struct HealthActivityTargetData: Hashable {
240 } 240 }
241 } 241 }
242 242
  243 +/// Generated class from Pigeon that represents data sent in messages.
  244 +struct HealthAuthorization: Hashable {
  245 + /// -1: AppleHealth不可用, 0: 需要请求授权, 1: 已被授权且能获取到部分数据, 2: 已被拒绝或者没有数据
  246 + var status: Int64
  247 + var hasData: Bool
  248 +
  249 +
  250 + // swift-format-ignore: AlwaysUseLowerCamelCase
  251 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthAuthorization? {
  252 + let status = pigeonVar_list[0] as! Int64
  253 + let hasData = pigeonVar_list[1] as! Bool
  254 +
  255 + return HealthAuthorization(
  256 + status: status,
  257 + hasData: hasData
  258 + )
  259 + }
  260 + func toList() -> [Any?] {
  261 + return [
  262 + status,
  263 + hasData,
  264 + ]
  265 + }
  266 + static func == (lhs: HealthAuthorization, rhs: HealthAuthorization) -> Bool {
  267 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  268 + func hash(into hasher: inout Hasher) {
  269 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  270 + }
  271 +}
  272 +
243 private class HealthKitApiPigeonCodecReader: FlutterStandardReader { 273 private class HealthKitApiPigeonCodecReader: FlutterStandardReader {
244 override func readValue(ofType type: UInt8) -> Any? { 274 override func readValue(ofType type: UInt8) -> Any? {
245 switch type { 275 switch type {
@@ -251,6 +281,8 @@ private class HealthKitApiPigeonCodecReader: FlutterStandardReader { @@ -251,6 +281,8 @@ private class HealthKitApiPigeonCodecReader: FlutterStandardReader {
251 return HealthSleepUploadDataPoint.fromList(self.readValue() as! [Any?]) 281 return HealthSleepUploadDataPoint.fromList(self.readValue() as! [Any?])
252 case 132: 282 case 132:
253 return HealthActivityTargetData.fromList(self.readValue() as! [Any?]) 283 return HealthActivityTargetData.fromList(self.readValue() as! [Any?])
  284 + case 133:
  285 + return HealthAuthorization.fromList(self.readValue() as! [Any?])
254 default: 286 default:
255 return super.readValue(ofType: type) 287 return super.readValue(ofType: type)
256 } 288 }
@@ -271,6 +303,9 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter { @@ -271,6 +303,9 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter {
271 } else if let value = value as? HealthActivityTargetData { 303 } else if let value = value as? HealthActivityTargetData {
272 super.writeByte(132) 304 super.writeByte(132)
273 super.writeValue(value.toList()) 305 super.writeValue(value.toList())
  306 + } else if let value = value as? HealthAuthorization {
  307 + super.writeByte(133)
  308 + super.writeValue(value.toList())
274 } else { 309 } else {
275 super.writeValue(value) 310 super.writeValue(value)
276 } 311 }
@@ -294,13 +329,13 @@ class HealthKitApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable @@ -294,13 +329,13 @@ class HealthKitApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
294 329
295 /// Generated protocol from Pigeon that represents a handler of messages from Flutter. 330 /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
296 protocol HealthKitHostApi { 331 protocol HealthKitHostApi {
297 - func checkHealthAppAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)  
298 func getHealthServerAuthUrl(completion: @escaping (Result<String, Error>) -> Void) 332 func getHealthServerAuthUrl(completion: @escaping (Result<String, Error>) -> Void)
299 - /// Opens Huawei Health client authorization UI. Returns whether user granted.  
300 - func requestHealthClientAuthorization() throws -> Bool  
301 func cancelHealthAppAuthorization() throws -> Bool 333 func cancelHealthAppAuthorization() throws -> Bool
302 /// Runs native health read and server upload pipeline. 334 /// Runs native health read and server upload pipeline.
303 func performHealthUpload() throws -> HealthUploadResult 335 func performHealthUpload() throws -> HealthUploadResult
  336 + /// Opens Huawei Health client authorization UI. Returns whether user granted.
  337 + func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void)
  338 + func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)
304 func fetchHrvData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) 339 func fetchHrvData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void)
305 func fetchHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) 340 func fetchHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void)
306 func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) 341 func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void)
@@ -324,21 +359,6 @@ class HealthKitHostApiSetup { @@ -324,21 +359,6 @@ class HealthKitHostApiSetup {
324 /// Sets up an instance of `HealthKitHostApi` to handle messages through the `binaryMessenger`. 359 /// Sets up an instance of `HealthKitHostApi` to handle messages through the `binaryMessenger`.
325 static func setUp(binaryMessenger: FlutterBinaryMessenger, api: HealthKitHostApi?, messageChannelSuffix: String = "") { 360 static func setUp(binaryMessenger: FlutterBinaryMessenger, api: HealthKitHostApi?, messageChannelSuffix: String = "") {
326 let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" 361 let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
327 - let checkHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)  
328 - if let api = api {  
329 - checkHealthAppAuthorizationChannel.setMessageHandler { _, reply in  
330 - api.checkHealthAppAuthorization { result in  
331 - switch result {  
332 - case .success(let res):  
333 - reply(wrapResult(res))  
334 - case .failure(let error):  
335 - reply(wrapError(error))  
336 - }  
337 - }  
338 - }  
339 - } else {  
340 - checkHealthAppAuthorizationChannel.setMessageHandler(nil)  
341 - }  
342 let getHealthServerAuthUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 362 let getHealthServerAuthUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
343 if let api = api { 363 if let api = api {
344 getHealthServerAuthUrlChannel.setMessageHandler { _, reply in 364 getHealthServerAuthUrlChannel.setMessageHandler { _, reply in
@@ -354,20 +374,6 @@ class HealthKitHostApiSetup { @@ -354,20 +374,6 @@ class HealthKitHostApiSetup {
354 } else { 374 } else {
355 getHealthServerAuthUrlChannel.setMessageHandler(nil) 375 getHealthServerAuthUrlChannel.setMessageHandler(nil)
356 } 376 }
357 - /// Opens Huawei Health client authorization UI. Returns whether user granted.  
358 - let requestHealthClientAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)  
359 - if let api = api {  
360 - requestHealthClientAuthorizationChannel.setMessageHandler { _, reply in  
361 - do {  
362 - let result = try api.requestHealthClientAuthorization()  
363 - reply(wrapResult(result))  
364 - } catch {  
365 - reply(wrapError(error))  
366 - }  
367 - }  
368 - } else {  
369 - requestHealthClientAuthorizationChannel.setMessageHandler(nil)  
370 - }  
371 let cancelHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 377 let cancelHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
372 if let api = api { 378 if let api = api {
373 cancelHealthAppAuthorizationChannel.setMessageHandler { _, reply in 379 cancelHealthAppAuthorizationChannel.setMessageHandler { _, reply in
@@ -395,6 +401,37 @@ class HealthKitHostApiSetup { @@ -395,6 +401,37 @@ class HealthKitHostApiSetup {
395 } else { 401 } else {
396 performHealthUploadChannel.setMessageHandler(nil) 402 performHealthUploadChannel.setMessageHandler(nil)
397 } 403 }
  404 + /// Opens Huawei Health client authorization UI. Returns whether user granted.
  405 + let checkHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  406 + if let api = api {
  407 + checkHealthAppAuthorizationChannel.setMessageHandler { _, reply in
  408 + api.checkHealthAppAuthorization { result in
  409 + switch result {
  410 + case .success(let res):
  411 + reply(wrapResult(res))
  412 + case .failure(let error):
  413 + reply(wrapError(error))
  414 + }
  415 + }
  416 + }
  417 + } else {
  418 + checkHealthAppAuthorizationChannel.setMessageHandler(nil)
  419 + }
  420 + let requestHealthClientAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  421 + if let api = api {
  422 + requestHealthClientAuthorizationChannel.setMessageHandler { _, reply in
  423 + api.requestHealthClientAuthorization { result in
  424 + switch result {
  425 + case .success(let res):
  426 + reply(wrapResult(res))
  427 + case .failure(let error):
  428 + reply(wrapError(error))
  429 + }
  430 + }
  431 + }
  432 + } else {
  433 + requestHealthClientAuthorizationChannel.setMessageHandler(nil)
  434 + }
398 let fetchHrvDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHrvData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 435 let fetchHrvDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHrvData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
399 if let api = api { 436 if let api = api {
400 fetchHrvDataChannel.setMessageHandler { message, reply in 437 fetchHrvDataChannel.setMessageHandler { message, reply in
1 import Foundation 1 import Foundation
  2 +import HealthKit
2 3
3 final class HealthKitHostApiImpl: HealthKitHostApi { 4 final class HealthKitHostApiImpl: HealthKitHostApi {
4 private let service: HealthKitService 5 private let service: HealthKitService
@@ -7,38 +8,68 @@ final class HealthKitHostApiImpl: HealthKitHostApi { @@ -7,38 +8,68 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
7 self.service = service 8 self.service = service
8 } 9 }
9 10
10 - func checkHealthAppAuthorization(completion: @escaping (Result<Bool, Error>) -> Void) {  
11 - Task {  
12 - let shouldAuth = await service.shouldRequestAuthorization()  
13 - let isAuthorized = service.isHealthDataAvailable && !shouldAuth  
14 - completion(.success(isAuthorized))  
15 - }  
16 - }  
17 -  
18 func getHealthServerAuthUrl(completion: @escaping (Result<String, Error>) -> Void) { 11 func getHealthServerAuthUrl(completion: @escaping (Result<String, Error>) -> Void) {
19 // Apple Health authorization is system-managed, not URL based. 12 // Apple Health authorization is system-managed, not URL based.
20 completion(.success("")) 13 completion(.success(""))
21 } 14 }
22 15
23 - func requestHealthClientAuthorization() throws -> Bool {  
24 - let result = runAuthorizationRequest()  
25 - if result.success {  
26 - service.startBackgroundObserversIfNeeded()  
27 - Task {  
28 - await service.refreshSharedWatchValues()  
29 - }  
30 - }  
31 - if let error = result.error {  
32 - throw error  
33 - }  
34 - return result.success  
35 - }  
36 -  
37 func cancelHealthAppAuthorization() throws -> Bool { 16 func cancelHealthAppAuthorization() throws -> Bool {
38 // iOS does not let apps revoke HealthKit permission programmatically. 17 // iOS does not let apps revoke HealthKit permission programmatically.
39 // Users must revoke access in Settings > Health > Data Access & Devices. 18 // Users must revoke access in Settings > Health > Data Access & Devices.
40 false 19 false
41 } 20 }
  21 +
  22 + func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, any Error>) -> Void) {
  23 + Task {
  24 + guard service.isHealthDataAvailable else {
  25 + completion(.success(HealthAuthorization(status: -1, hasData: false)))
  26 + return
  27 + }
  28 +
  29 + let requestStatus = await service.authorizationRequestStatus()
  30 + let status: Int64
  31 + var hasData = false
  32 + if requestStatus == .shouldRequest {
  33 + status = 0
  34 + } else {
  35 + hasData = await service.hasAnyReadableData()
  36 + status = hasData ? 1 : 2
  37 + }
  38 + DebugLogger.log(desc: "checkHealthAppAuthorization result: requestStatus=\(status) hasData=\(hasData)")
  39 + completion(.success(HealthAuthorization(status: status, hasData: hasData)))
  40 + }
  41 + }
  42 +
  43 + func requestHealthClientAuthorization(completion: @escaping (Result<Bool, any Error>) -> Void) {
  44 + DebugLogger.log(desc:"requestHealthClientAuthorization")
  45 + service.requestAuthorization { [service] success, error in
  46 + if let error {
  47 + DebugLogger.log(desc:"requestHealthClientAuthorization error: \(error)")
  48 + completion(.failure(error))
  49 + return
  50 + }
  51 +
  52 + Task {
  53 + let requestStatus = await service.authorizationRequestStatus()
  54 +
  55 + let status: Int64
  56 + if requestStatus == .shouldRequest {
  57 + status = 0
  58 + } else {
  59 + let hasData = await service.hasAnyReadableData()
  60 + status = hasData ? 1 : 2
  61 + }
  62 +
  63 + let granted = status == 1
  64 + DebugLogger.log(desc:"requestHealthClientAuthorization granted: \(granted)")
  65 + if granted {
  66 + service.startBackgroundObserversIfNeeded()
  67 + await service.refreshSharedWatchValues()
  68 + }
  69 + completion(.success(granted))
  70 + }
  71 + }
  72 + }
42 73
43 func performHealthUpload() throws -> HealthUploadResult { 74 func performHealthUpload() throws -> HealthUploadResult {
44 let summary = runBlocking { 75 let summary = runBlocking {
@@ -112,6 +112,14 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) { @@ -112,6 +112,14 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) {
112 112
113 113
114 114
  115 +enum AppleProductPaymentErrorMsg: Int {
  116 + case missingUUID = 0
  117 + case productNotFound = 1
  118 + case userCancelled = 2
  119 + case failedVerification = 3
  120 + case unknown = 4
  121 +}
  122 +
115 /// Generated class from Pigeon that represents data sent in messages. 123 /// Generated class from Pigeon that represents data sent in messages.
116 struct AppleSignInModel: Hashable { 124 struct AppleSignInModel: Hashable {
117 var userId: String 125 var userId: String
@@ -149,11 +157,114 @@ struct AppleSignInModel: Hashable { @@ -149,11 +157,114 @@ struct AppleSignInModel: Hashable {
149 } 157 }
150 } 158 }
151 159
  160 +/// Generated class from Pigeon that represents data sent in messages.
  161 +struct AppleProductInfo: Hashable {
  162 + var productId: String
  163 + var originPriceDescription: String
  164 + var priceDescription: String? = nil
  165 + var originPrice: Double
  166 + var price: Double? = nil
  167 + var isTrialPeriod: Bool
  168 +
  169 +
  170 + // swift-format-ignore: AlwaysUseLowerCamelCase
  171 + static func fromList(_ pigeonVar_list: [Any?]) -> AppleProductInfo? {
  172 + let productId = pigeonVar_list[0] as! String
  173 + let originPriceDescription = pigeonVar_list[1] as! String
  174 + let priceDescription: String? = nilOrValue(pigeonVar_list[2])
  175 + let originPrice = pigeonVar_list[3] as! Double
  176 + let price: Double? = nilOrValue(pigeonVar_list[4])
  177 + let isTrialPeriod = pigeonVar_list[5] as! Bool
  178 +
  179 + return AppleProductInfo(
  180 + productId: productId,
  181 + originPriceDescription: originPriceDescription,
  182 + priceDescription: priceDescription,
  183 + originPrice: originPrice,
  184 + price: price,
  185 + isTrialPeriod: isTrialPeriod
  186 + )
  187 + }
  188 + func toList() -> [Any?] {
  189 + return [
  190 + productId,
  191 + originPriceDescription,
  192 + priceDescription,
  193 + originPrice,
  194 + price,
  195 + isTrialPeriod,
  196 + ]
  197 + }
  198 + static func == (lhs: AppleProductInfo, rhs: AppleProductInfo) -> Bool {
  199 + return deepEqualsPlatformApi(lhs.toList(), rhs.toList()) }
  200 + func hash(into hasher: inout Hasher) {
  201 + deepHashPlatformApi(value: toList(), hasher: &hasher)
  202 + }
  203 +}
  204 +
  205 +/// Generated class from Pigeon that represents data sent in messages.
  206 +struct AppleProductPaymentResult: Hashable {
  207 + var productId: String
  208 + var appAccountToken: String? = nil
  209 + var originalTransactionId: String? = nil
  210 + var transactionId: String? = nil
  211 + var success: Bool? = nil
  212 + /// 错误描述:
  213 + /// 和AppleProductPaymentErrorMsg匹配的flutter处理,
  214 + /// 不匹配的则是StoreKit 直接给的错误描述,直接展示即可
  215 + var errorMessage: String? = nil
  216 +
  217 +
  218 + // swift-format-ignore: AlwaysUseLowerCamelCase
  219 + static func fromList(_ pigeonVar_list: [Any?]) -> AppleProductPaymentResult? {
  220 + let productId = pigeonVar_list[0] as! String
  221 + let appAccountToken: String? = nilOrValue(pigeonVar_list[1])
  222 + let originalTransactionId: String? = nilOrValue(pigeonVar_list[2])
  223 + let transactionId: String? = nilOrValue(pigeonVar_list[3])
  224 + let success: Bool? = nilOrValue(pigeonVar_list[4])
  225 + let errorMessage: String? = nilOrValue(pigeonVar_list[5])
  226 +
  227 + return AppleProductPaymentResult(
  228 + productId: productId,
  229 + appAccountToken: appAccountToken,
  230 + originalTransactionId: originalTransactionId,
  231 + transactionId: transactionId,
  232 + success: success,
  233 + errorMessage: errorMessage
  234 + )
  235 + }
  236 + func toList() -> [Any?] {
  237 + return [
  238 + productId,
  239 + appAccountToken,
  240 + originalTransactionId,
  241 + transactionId,
  242 + success,
  243 + errorMessage,
  244 + ]
  245 + }
  246 + static func == (lhs: AppleProductPaymentResult, rhs: AppleProductPaymentResult) -> Bool {
  247 + return deepEqualsPlatformApi(lhs.toList(), rhs.toList()) }
  248 + func hash(into hasher: inout Hasher) {
  249 + deepHashPlatformApi(value: toList(), hasher: &hasher)
  250 + }
  251 +}
  252 +
152 private class PlatformApiPigeonCodecReader: FlutterStandardReader { 253 private class PlatformApiPigeonCodecReader: FlutterStandardReader {
153 override func readValue(ofType type: UInt8) -> Any? { 254 override func readValue(ofType type: UInt8) -> Any? {
154 switch type { 255 switch type {
155 case 129: 256 case 129:
  257 + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
  258 + if let enumResultAsInt = enumResultAsInt {
  259 + return AppleProductPaymentErrorMsg(rawValue: enumResultAsInt)
  260 + }
  261 + return nil
  262 + case 130:
156 return AppleSignInModel.fromList(self.readValue() as! [Any?]) 263 return AppleSignInModel.fromList(self.readValue() as! [Any?])
  264 + case 131:
  265 + return AppleProductInfo.fromList(self.readValue() as! [Any?])
  266 + case 132:
  267 + return AppleProductPaymentResult.fromList(self.readValue() as! [Any?])
157 default: 268 default:
158 return super.readValue(ofType: type) 269 return super.readValue(ofType: type)
159 } 270 }
@@ -162,8 +273,17 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { @@ -162,8 +273,17 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader {
162 273
163 private class PlatformApiPigeonCodecWriter: FlutterStandardWriter { 274 private class PlatformApiPigeonCodecWriter: FlutterStandardWriter {
164 override func writeValue(_ value: Any) { 275 override func writeValue(_ value: Any) {
165 - if let value = value as? AppleSignInModel { 276 + if let value = value as? AppleProductPaymentErrorMsg {
166 super.writeByte(129) 277 super.writeByte(129)
  278 + super.writeValue(value.rawValue)
  279 + } else if let value = value as? AppleSignInModel {
  280 + super.writeByte(130)
  281 + super.writeValue(value.toList())
  282 + } else if let value = value as? AppleProductInfo {
  283 + super.writeByte(131)
  284 + super.writeValue(value.toList())
  285 + } else if let value = value as? AppleProductPaymentResult {
  286 + super.writeByte(132)
167 super.writeValue(value.toList()) 287 super.writeValue(value.toList())
168 } else { 288 } else {
169 super.writeValue(value) 289 super.writeValue(value)
@@ -191,7 +311,12 @@ protocol PlatformHostApi { @@ -191,7 +311,12 @@ protocol PlatformHostApi {
191 /// 返回完整的 User-Agent 字符串,由 native 侧组装: 311 /// 返回完整的 User-Agent 字符串,由 native 侧组装:
192 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 312 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
193 func getFullUserAgent() throws -> String 313 func getFullUserAgent() throws -> String
  314 + /// 请求苹果登录
194 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void) 315 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void)
  316 + /// 查询指定id的苹果商品
  317 + func requestAppleProductInfo(productId: String, completion: @escaping (Result<AppleProductInfo?, Error>) -> Void)
  318 + /// 从服务器下单后请求苹果支付
  319 + func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void)
195 } 320 }
196 321
197 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 322 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -215,6 +340,7 @@ class PlatformHostApiSetup { @@ -215,6 +340,7 @@ class PlatformHostApiSetup {
215 } else { 340 } else {
216 getFullUserAgentChannel.setMessageHandler(nil) 341 getFullUserAgentChannel.setMessageHandler(nil)
217 } 342 }
  343 + /// 请求苹果登录
218 let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 344 let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
219 if let api = api { 345 if let api = api {
220 requestAppleSignInChannel.setMessageHandler { _, reply in 346 requestAppleSignInChannel.setMessageHandler { _, reply in
@@ -230,5 +356,42 @@ class PlatformHostApiSetup { @@ -230,5 +356,42 @@ class PlatformHostApiSetup {
230 } else { 356 } else {
231 requestAppleSignInChannel.setMessageHandler(nil) 357 requestAppleSignInChannel.setMessageHandler(nil)
232 } 358 }
  359 + /// 查询指定id的苹果商品
  360 + let requestAppleProductInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleProductInfo\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  361 + if let api = api {
  362 + requestAppleProductInfoChannel.setMessageHandler { message, reply in
  363 + let args = message as! [Any?]
  364 + let productIdArg = args[0] as! String
  365 + api.requestAppleProductInfo(productId: productIdArg) { result in
  366 + switch result {
  367 + case .success(let res):
  368 + reply(wrapResult(res))
  369 + case .failure(let error):
  370 + reply(wrapError(error))
  371 + }
  372 + }
  373 + }
  374 + } else {
  375 + requestAppleProductInfoChannel.setMessageHandler(nil)
  376 + }
  377 + /// 从服务器下单后请求苹果支付
  378 + let performApplePaymentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performApplePayment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  379 + if let api = api {
  380 + performApplePaymentChannel.setMessageHandler { message, reply in
  381 + let args = message as! [Any?]
  382 + let productIdArg = args[0] as! String
  383 + let uuidArg = args[1] as! String
  384 + api.performApplePayment(productId: productIdArg, uuid: uuidArg) { result in
  385 + switch result {
  386 + case .success(let res):
  387 + reply(wrapResult(res))
  388 + case .failure(let error):
  389 + reply(wrapError(error))
  390 + }
  391 + }
  392 + }
  393 + } else {
  394 + performApplePaymentChannel.setMessageHandler(nil)
  395 + }
233 } 396 }
234 } 397 }
1 import Foundation 1 import Foundation
2 import AuthenticationServices 2 import AuthenticationServices
  3 +import StoreKit
3 import UIKit 4 import UIKit
4 import WebKit 5 import WebKit
5 6
@@ -11,11 +12,56 @@ import WebKit @@ -11,11 +12,56 @@ import WebKit
11 * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` 12 * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)`
12 */ 13 */
13 final class PlatformHostApiImpl: PlatformHostApi { 14 final class PlatformHostApiImpl: PlatformHostApi {
  15 + func requestAppleProductInfo(productId: String, completion: @escaping (Result<AppleProductInfo?, any Error>) -> Void) {
  16 + Task {
  17 + do {
  18 + let product = try await ApplePayment.shared.requestProducts(productId)
  19 + let isTrialPeriod = await ApplePayment.shared.isFreeTrail(product: product)
  20 + let originPrice = product.price
  21 + var displayPrice = product.displayPrice
  22 + var price = originPrice
  23 + if let introductoryOffer = product.subscription?.introductoryOffer, introductoryOffer.price > 0{
  24 + price = introductoryOffer.price
  25 + displayPrice = introductoryOffer.displayPrice
  26 + }else if let offer = product.subscription?.promotionalOffers.first(where: { $0.id == product.id }){
  27 + price = offer.price
  28 + displayPrice = offer.displayPrice
  29 + }
  30 +
  31 + let productInfo = AppleProductInfo(
  32 + productId: product.id,
  33 + originPriceDescription: product.displayPrice,
  34 + priceDescription: displayPrice,
  35 + originPrice: NSDecimalNumber(decimal: originPrice).doubleValue,
  36 + price: NSDecimalNumber(decimal: price).doubleValue,
  37 + isTrialPeriod: isTrialPeriod
  38 + )
  39 + print("[PlatformHostApiImpl.requestAppleProductInfo] return: \(productInfo)")
  40 + completion(.success(productInfo))
  41 + } catch StoreKitError.productNotFound {
  42 + print("[PlatformHostApiImpl.requestAppleProductInfo] return: nil")
  43 + completion(.success(nil))
  44 + } catch {
  45 + print("[PlatformHostApiImpl.requestAppleProductInfo] error: \(error.localizedDescription)")
  46 + completion(.failure(error))
  47 + }
  48 + }
  49 + }
  50 +
  51 + func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, any Error>) -> Void) {
  52 + Task {
  53 + let result = await ApplePayment.shared.purchase(productId, uuidString: uuid)
  54 + print("[PlatformHostApiImpl.performApplePayment] return: \(result)")
  55 + completion(.success(result))
  56 + }
  57 + }
  58 +
14 private var appleSignInCoordinator: AppleSignInCoordinator? 59 private var appleSignInCoordinator: AppleSignInCoordinator?
15 60
16 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, any Error>) -> Void) { 61 func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, any Error>) -> Void) {
17 DispatchQueue.main.async { [weak self] in 62 DispatchQueue.main.async { [weak self] in
18 guard let self else { 63 guard let self else {
  64 + print("[PlatformHostApiImpl.requestAppleSignIn] error: \(PlatformHostApiError.deallocated.localizedDescription)")
19 completion(.failure(PlatformHostApiError.deallocated)) 65 completion(.failure(PlatformHostApiError.deallocated))
20 return 66 return
21 } 67 }
@@ -25,12 +71,19 @@ final class PlatformHostApiImpl: PlatformHostApi { @@ -25,12 +71,19 @@ final class PlatformHostApiImpl: PlatformHostApi {
25 request.requestedScopes = [.fullName, .email] 71 request.requestedScopes = [.fullName, .email]
26 72
27 guard let presentationAnchor = AppleSignInCoordinator.currentPresentationAnchor() else { 73 guard let presentationAnchor = AppleSignInCoordinator.currentPresentationAnchor() else {
  74 + print("[PlatformHostApiImpl.requestAppleSignIn] error: \(PlatformHostApiError.missingPresentationAnchor.localizedDescription)")
28 completion(.failure(PlatformHostApiError.missingPresentationAnchor)) 75 completion(.failure(PlatformHostApiError.missingPresentationAnchor))
29 return 76 return
30 } 77 }
31 78
32 let coordinator = AppleSignInCoordinator(presentationAnchor: presentationAnchor) { [weak self] result in 79 let coordinator = AppleSignInCoordinator(presentationAnchor: presentationAnchor) { [weak self] result in
33 self?.appleSignInCoordinator = nil 80 self?.appleSignInCoordinator = nil
  81 + switch result {
  82 + case .success(let model):
  83 + print("[PlatformHostApiImpl.requestAppleSignIn] return: \(String(describing: model))")
  84 + case .failure(let error):
  85 + print("[PlatformHostApiImpl.requestAppleSignIn] error: \(error.localizedDescription)")
  86 + }
34 completion(result) 87 completion(result)
35 } 88 }
36 appleSignInCoordinator = coordinator 89 appleSignInCoordinator = coordinator
@@ -43,7 +96,8 @@ final class PlatformHostApiImpl: PlatformHostApi { @@ -43,7 +96,8 @@ final class PlatformHostApiImpl: PlatformHostApi {
43 } 96 }
44 97
45 func getFullUserAgent() throws -> String { 98 func getFullUserAgent() throws -> String {
46 - return UserAgent.share.finalUA 99 + let userAgent = UserAgent.share.finalUA
  100 + print("[PlatformHostApiImpl.getFullUserAgent] return: \(userAgent)")
  101 + return userAgent
47 } 102 }
48 } 103 }
49 -  
@@ -15,6 +15,9 @@ struct RunnerApp: App { @@ -15,6 +15,9 @@ struct RunnerApp: App {
15 WindowGroup { 15 WindowGroup {
16 FlutterRootView(engine:appDelegate.flutterEngine ) 16 FlutterRootView(engine:appDelegate.flutterEngine )
17 .ignoresSafeArea() 17 .ignoresSafeArea()
  18 + .onAppear {
  19 + _ = ApplePayment.shared.listenForTransactions()
  20 + }
18 } 21 }
19 } 22 }
20 } 23 }
  1 +import 'dart:convert';
  2 +
  3 +import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
  4 +
  5 +import 'models/health_upload_data_type.dart';
  6 +
  7 +class AppleHealthUploadLocalRecorder {
  8 + AppleHealthUploadLocalRecorder(this._storage);
  9 +
  10 + final UserAccountStorage _storage;
  11 +
  12 + AppleHealthUploadLocalRecordList load(int userId) {
  13 + final raw = _storage.appleHealthUploadLocalRecordJson(userId);
  14 + if (raw == null || raw.isEmpty) {
  15 + return const AppleHealthUploadLocalRecordList(records: []);
  16 + }
  17 +
  18 + try {
  19 + return AppleHealthUploadLocalRecordList.fromJson(
  20 + jsonDecode(raw) as Map<String, dynamic>,
  21 + );
  22 + } catch (_) {
  23 + return const AppleHealthUploadLocalRecordList(records: []);
  24 + }
  25 + }
  26 +
  27 + Future<void> saveLatestDataTime({
  28 + required int userId,
  29 + required HealthUploadDataType dataType,
  30 + required int latestDataTime,
  31 + }) async {
  32 + final records = [...load(userId).records];
  33 + records.removeWhere((item) => item.dataType == dataType);
  34 + records.add(
  35 + AppleHealthUploadLocalRecord(
  36 + dataType: dataType,
  37 + latestDataTime: latestDataTime,
  38 + ),
  39 + );
  40 + await _save(userId, AppleHealthUploadLocalRecordList(records: records));
  41 + }
  42 +
  43 + Future<void> saveLatestDataTimes({
  44 + required int userId,
  45 + required Map<HealthUploadDataType, int> latestDataTimes,
  46 + }) async {
  47 + if (latestDataTimes.isEmpty) return;
  48 +
  49 + final records = [...load(userId).records];
  50 + for (final entry in latestDataTimes.entries) {
  51 + records.removeWhere((item) => item.dataType == entry.key);
  52 + records.add(
  53 + AppleHealthUploadLocalRecord(
  54 + dataType: entry.key,
  55 + latestDataTime: entry.value,
  56 + ),
  57 + );
  58 + }
  59 + await _save(userId, AppleHealthUploadLocalRecordList(records: records));
  60 + }
  61 +
  62 + Future<void> clear(int userId) {
  63 + return _storage.clearAppleHealthUploadLocalRecord(userId);
  64 + }
  65 +
  66 + Future<void> _save(
  67 + int userId,
  68 + AppleHealthUploadLocalRecordList records,
  69 + ) {
  70 + return _storage.saveAppleHealthUploadLocalRecordJson(
  71 + userId,
  72 + jsonEncode(records.toJson()),
  73 + );
  74 + }
  75 +}
  76 +
  77 +class AppleHealthUploadLocalRecordList {
  78 + const AppleHealthUploadLocalRecordList({required this.records});
  79 +
  80 + final List<AppleHealthUploadLocalRecord> records;
  81 +
  82 + factory AppleHealthUploadLocalRecordList.fromJson(
  83 + Map<String, dynamic> json,
  84 + ) {
  85 + return AppleHealthUploadLocalRecordList(
  86 + records: (json['latest_data_time_list'] as List<dynamic>? ?? [])
  87 + .whereType<Map<String, dynamic>>()
  88 + .map(AppleHealthUploadLocalRecord.fromJson)
  89 + .where((item) => item.dataType != HealthUploadDataType.unknown)
  90 + .toList(),
  91 + );
  92 + }
  93 +
  94 + Map<String, dynamic> toJson() {
  95 + return {
  96 + 'latest_data_time_list': records.map((item) => item.toJson()).toList(),
  97 + };
  98 + }
  99 +
  100 + int? latestDataTime(HealthUploadDataType dataType) {
  101 + for (final record in records) {
  102 + if (record.dataType == dataType) return record.latestDataTime;
  103 + }
  104 + return null;
  105 + }
  106 +}
  107 +
  108 +class AppleHealthUploadLocalRecord {
  109 + const AppleHealthUploadLocalRecord({
  110 + required this.dataType,
  111 + required this.latestDataTime,
  112 + });
  113 +
  114 + final HealthUploadDataType dataType;
  115 + final int latestDataTime;
  116 +
  117 + factory AppleHealthUploadLocalRecord.fromJson(Map<String, dynamic> json) {
  118 + final rawDataType = json['data_type'];
  119 + return AppleHealthUploadLocalRecord(
  120 + dataType: rawDataType is int
  121 + ? HealthUploadDataType.fromValue(rawDataType)
  122 + : HealthUploadDataType.unknown,
  123 + latestDataTime: _intFromJson(json['latest_data_time']),
  124 + );
  125 + }
  126 +
  127 + Map<String, dynamic> toJson() {
  128 + return {
  129 + 'data_type': dataType.value,
  130 + 'latest_data_time': latestDataTime,
  131 + };
  132 + }
  133 +
  134 + static int _intFromJson(Object? value) {
  135 + if (value is int) return value;
  136 + if (value is double) return value.round();
  137 + if (value is String) return int.tryParse(value) ?? 0;
  138 + return 0;
  139 + }
  140 +}
1 import 'package:doublefeel_flutter/core/result/app_result.dart'; 1 import 'package:doublefeel_flutter/core/result/app_result.dart';
  2 +import 'package:doublefeel_flutter/core/logging/app_logger.dart';
  3 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  4 +import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
2 5
3 import 'apple_health_upload_api.dart'; 6 import 'apple_health_upload_api.dart';
  7 +import 'apple_health_upload_local_recorder.dart';
  8 +import 'health_kit_upload_mapper.dart';
4 import 'models/apple_health_upload_record.dart'; 9 import 'models/apple_health_upload_record.dart';
5 import 'models/apple_health_upload_request.dart'; 10 import 'models/apple_health_upload_request.dart';
6 import 'models/apple_health_upload_sample.dart'; 11 import 'models/apple_health_upload_sample.dart';
  12 +import 'models/health_upload_data_type.dart';
7 import 'models/upload_activity_target.dart'; 13 import 'models/upload_activity_target.dart';
8 import 'models/upload_sleep.dart'; 14 import 'models/upload_sleep.dart';
9 15
10 class AppleHealthUploadTool { 16 class AppleHealthUploadTool {
11 - AppleHealthUploadTool(this._api); 17 + AppleHealthUploadTool(
  18 + this._api,
  19 + this._localRecorder,
  20 + this._userPreferences, {
  21 + HealthKitHostApi? hostApi,
  22 + }) : _hostApi = hostApi ?? HealthKitHostApi();
12 23
13 final AppleHealthUploadApi _api; 24 final AppleHealthUploadApi _api;
  25 + final AppleHealthUploadLocalRecorder _localRecorder;
  26 + final UserPreferencesStorage _userPreferences;
  27 + final HealthKitHostApi _hostApi;
  28 +
  29 + static const _historyWindow = Duration(days: 365 * 2);
  30 + static const _commonTypes = [
  31 + HealthUploadDataType.hrv,
  32 + HealthUploadDataType.heartRate,
  33 + HealthUploadDataType.walkingHeartRate,
  34 + HealthUploadDataType.restingHeartRate,
  35 + HealthUploadDataType.sleepingHeartRate,
  36 + HealthUploadDataType.oxygenSaturation,
  37 + HealthUploadDataType.activeEnergy,
  38 + HealthUploadDataType.exercise,
  39 + HealthUploadDataType.stand,
  40 + HealthUploadDataType.steps,
  41 + HealthUploadDataType.sleepingWristTemperature,
  42 + HealthUploadDataType.respiratoryRate,
  43 + HealthUploadDataType.irregularHeartRhythm,
  44 + ];
14 45
15 Future<AppleHealthUploadResult> upload({ 46 Future<AppleHealthUploadResult> upload({
16 List<AppleHealthUploadSample> commonData = const [], 47 List<AppleHealthUploadSample> commonData = const [],
@@ -56,6 +87,229 @@ class AppleHealthUploadTool { @@ -56,6 +87,229 @@ class AppleHealthUploadTool {
56 getLatestSleepUploadRecord() { 87 getLatestSleepUploadRecord() {
57 return _api.getLatestSleepUploadRecord(); 88 return _api.getLatestSleepUploadRecord();
58 } 89 }
  90 +
  91 + Future<AppleHealthUploadResult> uploadAllNewDataFromHealthKit() async {
  92 + final userId = _userPreferences.preferences.value.meUserInfo?.id ?? 0;
  93 + if (userId <= 0) {
  94 + return const AppleHealthUploadResult(
  95 + commonUploadSuccess: false,
  96 + sleepUploadSuccess: false,
  97 + activityTargetUploadSuccess: false,
  98 + );
  99 + }
  100 +
  101 + final endTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  102 + final startTimes = await _resolveStartTimes(userId);
  103 +
  104 + final commonDataByType =
  105 + <HealthUploadDataType, List<AppleHealthUploadSample>>{};
  106 + for (final type in _commonTypes) {
  107 + final points = await _safeFetchCommonDataPointList(
  108 + type: type,
  109 + startTime: startTimes[type] ?? _fallbackStartTime(),
  110 + endTime: endTime,
  111 + );
  112 + final models = points
  113 + .map((point) => point.toAppleHealthUploadSample())
  114 + .whereType<AppleHealthUploadSample>()
  115 + .toList();
  116 + commonDataByType[type] = models;
  117 + }
  118 +
  119 + final sleepPoints = await _safeFetchSleepDataPointList(
  120 + startTimes[HealthUploadDataType.sleep] ?? _fallbackStartTime(),
  121 + endTime,
  122 + );
  123 + final sleepData =
  124 + sleepPoints.map((item) => item.toHealthSleepUploadData()).toList();
  125 +
  126 + final activityTarget = await _fetchActivityTarget(startTimes, endTime);
  127 +
  128 + final commonData = commonDataByType.values
  129 + .expand((items) => items)
  130 + .toList(growable: false);
  131 +
  132 + final result = await upload(
  133 + commonData: commonData,
  134 + sleepData: sleepData,
  135 + activityTarget: activityTarget,
  136 + );
  137 +
  138 + if (result.commonUploadSuccess) {
  139 + final uploadedCommonTypes = <HealthUploadDataType, int>{};
  140 + for (final entry in commonDataByType.entries) {
  141 + if (entry.value.isNotEmpty) {
  142 + uploadedCommonTypes[entry.key] = endTime;
  143 + }
  144 + }
  145 + await _localRecorder.saveLatestDataTimes(
  146 + userId: userId,
  147 + latestDataTimes: uploadedCommonTypes,
  148 + );
  149 + }
  150 +
  151 + if (result.sleepUploadSuccess && sleepData.isNotEmpty) {
  152 + await _localRecorder.saveLatestDataTime(
  153 + userId: userId,
  154 + dataType: HealthUploadDataType.sleep,
  155 + latestDataTime: endTime,
  156 + );
  157 + }
  158 +
  159 + return AppleHealthUploadResult(
  160 + commonUploadSuccess: result.commonUploadSuccess,
  161 + sleepUploadSuccess: result.sleepUploadSuccess,
  162 + activityTargetUploadSuccess: result.activityTargetUploadSuccess,
  163 + commonCount: commonData.length,
  164 + sleepCount: sleepData.length,
  165 + activityTargetCount: activityTarget == null ? 0 : 1,
  166 + );
  167 + }
  168 +
  169 + Future<Map<HealthUploadDataType, int>> _resolveStartTimes(int userId) async {
  170 + final localRecords = _localRecorder.load(userId);
  171 + final serverCommonRecords = await _serverCommonRecordMap();
  172 + final serverSleepRecord = await _serverSleepLatestTime();
  173 +
  174 + final result = <HealthUploadDataType, int>{};
  175 + for (final type in [..._commonTypes, HealthUploadDataType.sleep]) {
  176 + final serverTime = type == HealthUploadDataType.sleep
  177 + ? serverSleepRecord
  178 + : serverCommonRecords[type];
  179 + result[type] = _resolveStartTime(
  180 + localTime: localRecords.latestDataTime(type),
  181 + serverTime: serverTime,
  182 + );
  183 + }
  184 + return result;
  185 + }
  186 +
  187 + Future<Map<HealthUploadDataType, int>> _serverCommonRecordMap() async {
  188 + final result = await _api.getLatestCommonUploadRecordList(
  189 + errorHandlingPolicy: null,
  190 + );
  191 + if (result
  192 + case AppSuccess<AppleHealthLatestUploadRecordList>(data: final data)) {
  193 + final records = data.latestDataTimeList ?? const [];
  194 + return {
  195 + for (final record in records)
  196 + if (record.dataType != null && record.latestDataTime != null)
  197 + record.dataType!: record.latestDataTime!,
  198 + };
  199 + }
  200 + return {};
  201 + }
  202 +
  203 + Future<int?> _serverSleepLatestTime() async {
  204 + final result = await _api.getLatestSleepUploadRecord(
  205 + errorHandlingPolicy: null,
  206 + );
  207 + if (result
  208 + case AppSuccess<AppleHealthLatestSleepUploadRecord>(data: final data)) {
  209 + return data.latestDataTime;
  210 + }
  211 + return null;
  212 + }
  213 +
  214 + int _resolveStartTime({int? localTime, int? serverTime}) {
  215 + final fallback = _fallbackStartTime();
  216 + if (localTime != null && localTime > fallback) return localTime;
  217 + if (serverTime != null && serverTime > fallback) return serverTime;
  218 + return fallback;
  219 + }
  220 +
  221 + int _fallbackStartTime() {
  222 + final start = DateTime.now().subtract(_historyWindow);
  223 + return DateTime(start.year, start.month, start.day)
  224 + .millisecondsSinceEpoch ~/
  225 + 1000;
  226 + }
  227 +
  228 + Future<List<HealthUploadDataPoint>> _safeFetchCommonDataPointList({
  229 + required HealthUploadDataType type,
  230 + required int startTime,
  231 + required int endTime,
  232 + }) async {
  233 + try {
  234 + return await _fetchCommonDataPointList(type, startTime, endTime);
  235 + } catch (error, stackTrace) {
  236 + AppLogger.w(
  237 + 'Fetch Apple Health data failed: $type',
  238 + error,
  239 + stackTrace,
  240 + );
  241 + return const [];
  242 + }
  243 + }
  244 +
  245 + Future<List<HealthSleepUploadDataPoint>> _safeFetchSleepDataPointList(
  246 + int startTime,
  247 + int endTime,
  248 + ) async {
  249 + try {
  250 + return await _hostApi.fetchSleepData(startTime, endTime);
  251 + } catch (error, stackTrace) {
  252 + AppLogger.w('Fetch Apple Health sleep data failed', error, stackTrace);
  253 + return const [];
  254 + }
  255 + }
  256 +
  257 + Future<List<HealthUploadDataPoint>> _fetchCommonDataPointList(
  258 + HealthUploadDataType type,
  259 + int startTime,
  260 + int endTime,
  261 + ) {
  262 + return switch (type) {
  263 + HealthUploadDataType.hrv => _hostApi.fetchHrvData(startTime, endTime),
  264 + HealthUploadDataType.heartRate =>
  265 + _hostApi.fetchHeartRateData(startTime, endTime),
  266 + HealthUploadDataType.walkingHeartRate =>
  267 + _hostApi.fetchWalkingHeartRateData(startTime, endTime),
  268 + HealthUploadDataType.restingHeartRate =>
  269 + _hostApi.fetchRestingHeartRateData(startTime, endTime),
  270 + HealthUploadDataType.sleepingHeartRate =>
  271 + _hostApi.fetchSleepingHeartRateData(startTime, endTime),
  272 + HealthUploadDataType.oxygenSaturation =>
  273 + _hostApi.fetchOxygenSaturationData(startTime, endTime),
  274 + HealthUploadDataType.activeEnergy =>
  275 + _hostApi.fetchActiveEnergyData(startTime, endTime),
  276 + HealthUploadDataType.exercise =>
  277 + _hostApi.fetchExerciseData(startTime, endTime),
  278 + HealthUploadDataType.stand => _hostApi.fetchStandData(startTime, endTime),
  279 + HealthUploadDataType.steps =>
  280 + _hostApi.fetchStepCountData(startTime, endTime),
  281 + HealthUploadDataType.sleepingWristTemperature =>
  282 + _hostApi.fetchSleepingWristTemperatureData(startTime, endTime),
  283 + HealthUploadDataType.respiratoryRate =>
  284 + _hostApi.fetchRespiratoryRateData(startTime, endTime),
  285 + HealthUploadDataType.irregularHeartRhythm =>
  286 + _hostApi.fetchIrregularHeartRhythmData(startTime, endTime),
  287 + HealthUploadDataType.unknown ||
  288 + HealthUploadDataType.sleep =>
  289 + Future.value(const []),
  290 + };
  291 + }
  292 +
  293 + Future<HealthActivityTargetUploadData?> _fetchActivityTarget(
  294 + Map<HealthUploadDataType, int> startTimes,
  295 + int endTime,
  296 + ) async {
  297 + final startTime =
  298 + startTimes[HealthUploadDataType.activeEnergy] ?? _fallbackStartTime();
  299 + final HealthActivityTargetData? target;
  300 + try {
  301 + target = await _hostApi.fetchActivityTargetData(startTime, endTime);
  302 + } catch (error, stackTrace) {
  303 + AppLogger.w(
  304 + 'Fetch Apple Health activity target failed', error, stackTrace);
  305 + return null;
  306 + }
  307 + if (target == null) return null;
  308 + return HealthActivityTargetUploadData(
  309 + move: target.move,
  310 + stand: target.stand,
  311 + );
  312 + }
59 } 313 }
60 314
61 class AppleHealthUploadResult { 315 class AppleHealthUploadResult {
@@ -63,11 +317,17 @@ class AppleHealthUploadResult { @@ -63,11 +317,17 @@ class AppleHealthUploadResult {
63 required this.commonUploadSuccess, 317 required this.commonUploadSuccess,
64 required this.sleepUploadSuccess, 318 required this.sleepUploadSuccess,
65 required this.activityTargetUploadSuccess, 319 required this.activityTargetUploadSuccess,
  320 + this.commonCount = 0,
  321 + this.sleepCount = 0,
  322 + this.activityTargetCount = 0,
66 }); 323 });
67 324
68 final bool commonUploadSuccess; 325 final bool commonUploadSuccess;
69 final bool sleepUploadSuccess; 326 final bool sleepUploadSuccess;
70 final bool activityTargetUploadSuccess; 327 final bool activityTargetUploadSuccess;
  328 + final int commonCount;
  329 + final int sleepCount;
  330 + final int activityTargetCount;
71 331
72 bool get isSuccess => 332 bool get isSuccess =>
73 commonUploadSuccess && sleepUploadSuccess && activityTargetUploadSuccess; 333 commonUploadSuccess && sleepUploadSuccess && activityTargetUploadSuccess;
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 2
3 import '../apple_health_upload/apple_health_upload_api.dart'; 3 import '../apple_health_upload/apple_health_upload_api.dart';
  4 +import '../apple_health_upload/apple_health_upload_local_recorder.dart';
4 import '../apple_health_upload/apple_health_upload_tool.dart'; 5 import '../apple_health_upload/apple_health_upload_tool.dart';
5 import '../../core/config/app_environment_config.dart'; 6 import '../../core/config/app_environment_config.dart';
6 import '../../core/error/app_error_handler.dart'; 7 import '../../core/error/app_error_handler.dart';
@@ -75,7 +76,15 @@ void registerHealthDeps(DioClient dioClient) { @@ -75,7 +76,15 @@ void registerHealthDeps(DioClient dioClient) {
75 Get.lazyPut(() => HealthApi(dioClient), fenix: true); 76 Get.lazyPut(() => HealthApi(dioClient), fenix: true);
76 Get.lazyPut(() => AppleHealthUploadApi(dioClient), fenix: true); 77 Get.lazyPut(() => AppleHealthUploadApi(dioClient), fenix: true);
77 Get.lazyPut( 78 Get.lazyPut(
78 - () => AppleHealthUploadTool(Get.find<AppleHealthUploadApi>()), 79 + () => AppleHealthUploadLocalRecorder(Get.find<UserAccountStorage>()),
  80 + fenix: true,
  81 + );
  82 + Get.lazyPut(
  83 + () => AppleHealthUploadTool(
  84 + Get.find<AppleHealthUploadApi>(),
  85 + Get.find<AppleHealthUploadLocalRecorder>(),
  86 + Get.find<UserPreferencesStorage>(),
  87 + ),
79 fenix: true, 88 fenix: true,
80 ); 89 );
81 Get.lazyPut( 90 Get.lazyPut(
1 import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; 1 import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
  2 +import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
  3 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
2 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
3 5
4 import '../controllers/apple_health_upload_test_controller.dart'; 6 import '../controllers/apple_health_upload_test_controller.dart';
@@ -9,6 +11,8 @@ class AppleHealthUploadTestBinding extends Bindings { @@ -9,6 +11,8 @@ class AppleHealthUploadTestBinding extends Bindings {
9 Get.lazyPut<AppleHealthUploadTestController>( 11 Get.lazyPut<AppleHealthUploadTestController>(
10 () => AppleHealthUploadTestController( 12 () => AppleHealthUploadTestController(
11 Get.find<AppleHealthUploadTool>(), 13 Get.find<AppleHealthUploadTool>(),
  14 + Get.find<UserAccountStorage>(),
  15 + Get.find<UserPreferencesStorage>(),
12 ), 16 ),
13 ); 17 );
14 } 18 }
@@ -6,14 +6,22 @@ import 'package:doublefeel_flutter/app/apple_health_upload/models/apple_health_u @@ -6,14 +6,22 @@ import 'package:doublefeel_flutter/app/apple_health_upload/models/apple_health_u
6 import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_activity_target.dart'; 6 import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_activity_target.dart';
7 import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_sleep.dart'; 7 import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_sleep.dart';
8 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 8 import 'package:doublefeel_flutter/core/util/app_toast.dart';
  9 +import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
  10 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
9 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; 11 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
10 import 'package:flutter/foundation.dart'; 12 import 'package:flutter/foundation.dart';
11 import 'package:get/get.dart'; 13 import 'package:get/get.dart';
12 14
13 class AppleHealthUploadTestController extends GetxController { 15 class AppleHealthUploadTestController extends GetxController {
14 - AppleHealthUploadTestController(this._uploadTool); 16 + AppleHealthUploadTestController(
  17 + this._uploadTool,
  18 + this._userAccountStorage,
  19 + this._userPreferencesStorage,
  20 + );
15 21
16 final AppleHealthUploadTool _uploadTool; 22 final AppleHealthUploadTool _uploadTool;
  23 + final UserAccountStorage _userAccountStorage;
  24 + final UserPreferencesStorage _userPreferencesStorage;
17 final HealthKitHostApi _hostApi = HealthKitHostApi(); 25 final HealthKitHostApi _hostApi = HealthKitHostApi();
18 26
19 final logText = ''.obs; 27 final logText = ''.obs;
@@ -28,13 +36,23 @@ class AppleHealthUploadTestController extends GetxController { @@ -28,13 +36,23 @@ class AppleHealthUploadTestController extends GetxController {
28 Future<void> checkPermission() async { 36 Future<void> checkPermission() async {
29 _appendLog('开始检测 AppleHealth 权限'); 37 _appendLog('开始检测 AppleHealth 权限');
30 try { 38 try {
31 - final hasPermission = await _hostApi.checkHealthAppAuthorization();  
32 - _printHostApiResult('checkHealthAppAuthorization', hasPermission);  
33 - if (hasPermission) { 39 + final result = await _hostApi.checkHealthAppAuthorization();
  40 + _printHostApiResult('checkHealthAppAuthorization', result.hasData);
  41 + if (result.status == 0) {
  42 + permissionTitle.value = '权限:待请求权限';
  43 + _appendLog('AppleHealth 待请求权限');
  44 + return;
  45 + }
  46 + if (result.status == 1) {
34 permissionTitle.value = '权限:已授权'; 47 permissionTitle.value = '权限:已授权';
35 _appendLog('AppleHealth 权限已授权'); 48 _appendLog('AppleHealth 权限已授权');
36 return; 49 return;
37 } 50 }
  51 + if (result.status == 2) {
  52 + permissionTitle.value = '权限:已拒绝/无数据';
  53 + _appendLog('AppleHealth 已拒绝/无数据');
  54 + return;
  55 + }
38 56
39 final granted = await _hostApi.requestHealthClientAuthorization(); 57 final granted = await _hostApi.requestHealthClientAuthorization();
40 _printHostApiResult('requestHealthClientAuthorization', granted); 58 _printHostApiResult('requestHealthClientAuthorization', granted);
@@ -56,11 +74,9 @@ class AppleHealthUploadTestController extends GetxController { @@ -56,11 +74,9 @@ class AppleHealthUploadTestController extends GetxController {
56 _activityTarget = null; 74 _activityTarget = null;
57 _appendLog('开始同步 HealthKitHostApi 数据'); 75 _appendLog('开始同步 HealthKitHostApi 数据');
58 76
  77 + final userId = _currentUserId;
59 final endTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; 78 final endTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
60 - final startTime = DateTime.now()  
61 - .subtract(const Duration(days: 365 * 2))  
62 - .millisecondsSinceEpoch ~/  
63 - 1000; 79 + final startTime = _resolveSyncStartTime(userId);
64 _appendLog('同步范围:$startTime -> $endTime'); 80 _appendLog('同步范围:$startTime -> $endTime');
65 81
66 try { 82 try {
@@ -124,6 +140,13 @@ class AppleHealthUploadTestController extends GetxController { @@ -124,6 +140,13 @@ class AppleHealthUploadTestController extends GetxController {
124 '同步完成:common=${_commonData.length}, sleep=${_sleepData.length}, ' 140 '同步完成:common=${_commonData.length}, sleep=${_sleepData.length}, '
125 'activityTarget=${_activityTarget == null ? 0 : 1}', 141 'activityTarget=${_activityTarget == null ? 0 : 1}',
126 ); 142 );
  143 + if (userId > 0) {
  144 + await _userAccountStorage.saveAppleHealthUploadTestLastSyncTime(
  145 + userId,
  146 + endTime,
  147 + );
  148 + _appendLog('已保存本次同步时间:$endTime');
  149 + }
127 } catch (error, stackTrace) { 150 } catch (error, stackTrace) {
128 _appendLog('同步失败:$error'); 151 _appendLog('同步失败:$error');
129 _appendLog(stackTrace.toString()); 152 _appendLog(stackTrace.toString());
@@ -132,6 +155,23 @@ class AppleHealthUploadTestController extends GetxController { @@ -132,6 +155,23 @@ class AppleHealthUploadTestController extends GetxController {
132 } 155 }
133 } 156 }
134 157
  158 + int get _currentUserId =>
  159 + _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0;
  160 +
  161 + int _resolveSyncStartTime(int userId) {
  162 + if (userId > 0) {
  163 + final lastSyncTime =
  164 + _userAccountStorage.appleHealthUploadTestLastSyncTime(userId);
  165 + if (lastSyncTime != null && lastSyncTime > 0) {
  166 + return lastSyncTime;
  167 + }
  168 + }
  169 + final start = DateTime.now().subtract(const Duration(days: 365 * 2));
  170 + return DateTime(start.year, start.month, start.day)
  171 + .millisecondsSinceEpoch ~/
  172 + 1000;
  173 + }
  174 +
135 Future<void> uploadHealthData() async { 175 Future<void> uploadHealthData() async {
136 if (isUploading.value) return; 176 if (isUploading.value) return;
137 if (_commonData.isEmpty && _sleepData.isEmpty && _activityTarget == null) { 177 if (_commonData.isEmpty && _sleepData.isEmpty && _activityTarget == null) {
  1 +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
1 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; 2 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
2 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
3 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 4 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
@@ -20,6 +21,7 @@ class HomeBinding extends Bindings { @@ -20,6 +21,7 @@ class HomeBinding extends Bindings {
20 Get.find<HealthApi>(), 21 Get.find<HealthApi>(),
21 Get.find<UserStateService>(), 22 Get.find<UserStateService>(),
22 Get.find<HealthKitUploadService>(), 23 Get.find<HealthKitUploadService>(),
  24 + Get.find<AppleHealthUploadTool>(),
23 ), 25 ),
24 fenix: true, 26 fenix: true,
25 ); 27 );
1 import 'dart:async'; 1 import 'dart:async';
2 2
  3 +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
3 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; 4 import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
4 -import 'package:doublefeel_flutter/app/routes/app_pages.dart';  
5 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 5 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
6 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
7 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
@@ -12,6 +12,8 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; @@ -12,6 +12,8 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart';
12 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 12 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
13 import 'package:doublefeel_flutter/data/models/health/health_models.dart'; 13 import 'package:doublefeel_flutter/data/models/health/health_models.dart';
14 import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; 14 import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart';
  15 +import 'package:doublefeel_flutter/data/models/user/user_models.dart';
  16 +import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
15 import 'package:flutter/material.dart'; 17 import 'package:flutter/material.dart';
16 import 'package:get/get.dart'; 18 import 'package:get/get.dart';
17 19
@@ -44,12 +46,15 @@ class TodayController extends GetxController { @@ -44,12 +46,15 @@ class TodayController extends GetxController {
44 this._healthApi, 46 this._healthApi,
45 this._userStateService, 47 this._userStateService,
46 this._healthKitUploadService, 48 this._healthKitUploadService,
  49 + this._appleHealthUploadTool,
47 ); 50 );
48 51
49 final UserApi _userApi; 52 final UserApi _userApi;
50 final HealthApi _healthApi; 53 final HealthApi _healthApi;
51 final UserStateService _userStateService; 54 final UserStateService _userStateService;
52 final HealthKitUploadService _healthKitUploadService; 55 final HealthKitUploadService _healthKitUploadService;
  56 + final AppleHealthUploadTool _appleHealthUploadTool;
  57 + final HealthKitHostApi _hostApi = HealthKitHostApi();
53 58
54 UserStateService get userStateService => _userStateService; 59 UserStateService get userStateService => _userStateService;
55 60
@@ -105,18 +110,25 @@ class TodayController extends GetxController { @@ -105,18 +110,25 @@ class TodayController extends GetxController {
105 final hrvAnnotations = <HrvAnnotation>[].obs; 110 final hrvAnnotations = <HrvAnnotation>[].obs;
106 111
107 Future<void> requestHealthAuthorization() async { 112 Future<void> requestHealthAuthorization() async {
108 - // try {  
109 - // await _healthKitUploadService.requestClientAuthorization();  
110 - // await _refreshHealthAuthorizationState();  
111 - // } catch (error) {  
112 - // debugPrint('Health authorization skipped: $error');  
113 - // }  
114 - Get.to(NoHealthDataPage(  
115 - onRefresh: () {},  
116 - onHelp: () {  
117 - Get.toNamed(Routes.HELP);  
118 - },  
119 - )); 113 + try {
  114 + final result = await _hostApi.checkHealthAppAuthorization();
  115 + print("checkHealthAppAuthorization : $result");
  116 + if (result.status == 0) {
  117 + bool success = await _hostApi.requestHealthClientAuthorization();
  118 + print("requestHealthClientAuthorization : $success");
  119 + if (success) {
  120 + _performDataUpload();
  121 + } else {
  122 + Get.to(NoHealthDataPage(
  123 + onRefresh: _performDataUpload,
  124 + ));
  125 + }
  126 + return;
  127 + }
  128 + Get.to(NoHealthDataPage(
  129 + onRefresh: _performDataUpload,
  130 + ));
  131 + } catch (e) {}
120 } 132 }
121 133
122 @override 134 @override
@@ -152,7 +164,8 @@ class TodayController extends GetxController { @@ -152,7 +164,8 @@ class TodayController extends GetxController {
152 isLoadingToday.value = true; 164 isLoadingToday.value = true;
153 try { 165 try {
154 await Future.wait([ 166 await Future.wait([
155 - // _refreshHealthAuthorizationState(), 167 + _refreshUserGreeting(),
  168 + _refreshHealthAuthorizationState(),
156 _refreshHealthDataForDate(date), 169 _refreshHealthDataForDate(date),
157 ]); 170 ]);
158 } catch (error, stackTrace) { 171 } catch (error, stackTrace) {
@@ -164,23 +177,40 @@ class TodayController extends GetxController { @@ -164,23 +177,40 @@ class TodayController extends GetxController {
164 } 177 }
165 } 178 }
166 179
167 - Future<void> _refreshHealthAuthorizationState() async {  
168 - final serverAuth =  
169 - await _healthApi.checkServerHealthAuth(errorHandlingPolicy: null);  
170 - final hasServerAuth = switch (serverAuth) {  
171 - AppSuccess<HealthAuthResponse>(data: final auth) =>  
172 - auth.scope?.trim().isNotEmpty == true,  
173 - _ => false,  
174 - };  
175 -  
176 - bool hasClientAuth = false; 180 + /// 上传 Apple Health 新数据。
  181 + Future<void> _performDataUpload() async {
177 try { 182 try {
178 - hasClientAuth = await _healthKitUploadService.isHealthAuthorized(); 183 + final result =
  184 + await _appleHealthUploadTool.uploadAllNewDataFromHealthKit();
  185 + AppLogger.i(
  186 + 'Apple Health upload finished: '
  187 + 'common=${result.commonUploadSuccess}(${result.commonCount}), '
  188 + 'sleep=${result.sleepUploadSuccess}(${result.sleepCount}), '
  189 + 'activityTarget=${result.activityTargetUploadSuccess}'
  190 + '(${result.activityTargetCount}), '
  191 + 'success=${result.isSuccess}',
  192 + );
179 } catch (error, stackTrace) { 193 } catch (error, stackTrace) {
180 - AppLogger.w('Health authorization check failed', error, stackTrace); 194 + AppLogger.e('Apple Health upload failed', error, stackTrace);
  195 + }
  196 + }
  197 +
  198 + Future<void> _refreshUserGreeting() async {
  199 + final result = await _userApi.getUserInfo(errorHandlingPolicy: null);
  200 + if (result case AppSuccess<UserInfoResponse>(data: final user)) {
  201 + final name = user.nickname?.trim();
  202 + stressSubtitle.value = name == null || name.isEmpty
  203 + ? 'Hi, 你今日的综合压力状态'
  204 + : 'Hi, $name 今日的综合压力状态';
181 } 205 }
  206 + }
  207 +
  208 + Future<void> _refreshHealthAuthorizationState() async {
  209 + try {
  210 + final result = await _hostApi.checkHealthAppAuthorization();
182 211
183 - showHealthDataAuthCard.value = !(hasServerAuth || hasClientAuth); 212 + showHealthDataAuthCard.value = result.status != 1;
  213 + } catch (e) {}
184 } 214 }
185 215
186 Future<void> _refreshHealthDataForDate(DateTime date) async { 216 Future<void> _refreshHealthDataForDate(DateTime date) async {
@@ -182,6 +182,7 @@ final officialThemes = <WatchThemeItem>[ @@ -182,6 +182,7 @@ final officialThemes = <WatchThemeItem>[
182 ), 182 ),
183 ], 183 ],
184 ), 184 ),
  185 + /*
185 WatchThemeItem( 186 WatchThemeItem(
186 title: '垂耳粉兔', 187 title: '垂耳粉兔',
187 infoList: [ 188 infoList: [
@@ -266,6 +267,7 @@ final officialThemes = <WatchThemeItem>[ @@ -266,6 +267,7 @@ final officialThemes = <WatchThemeItem>[
266 ), 267 ),
267 ], 268 ],
268 ), 269 ),
  270 + */
269 ]; 271 ];
270 272
271 final customThemes = <WatchThemeItem>[ 273 final customThemes = <WatchThemeItem>[
@@ -17,10 +17,11 @@ class HealthKitUploadService { @@ -17,10 +17,11 @@ class HealthKitUploadService {
17 final HealthKitHostApi _healthKitHost; 17 final HealthKitHostApi _healthKitHost;
18 18
19 Future<bool> isHealthAuthorized() async { 19 Future<bool> isHealthAuthorized() async {
20 - if (!isAndroid) {  
21 - return false;  
22 - }  
23 - return _healthKitHost.checkHealthAppAuthorization(); 20 + // if (isAndroid) {
  21 + // return false;
  22 + // }
  23 + final result = await _healthKitHost.checkHealthAppAuthorization();
  24 + return result.status == 1;
24 } 25 }
25 26
26 Future<String> getServerAuthUrl() => _healthKitHost.getHealthServerAuthUrl(); 27 Future<String> getServerAuthUrl() => _healthKitHost.getHealthServerAuthUrl();
@@ -18,6 +18,14 @@ class UserAccountStorage { @@ -18,6 +18,14 @@ class UserAccountStorage {
18 static String _onboardingKey(int userId) => 18 static String _onboardingKey(int userId) =>
19 'account_onboarding_stage_$userId'; 19 'account_onboarding_stage_$userId';
20 20
  21 + /// Apple Health 上传时间记录 key,按 userId 隔离。
  22 + static String _appleHealthUploadRecordKey(int userId) =>
  23 + 'apple_health_upload_local_records_$userId';
  24 +
  25 + /// Apple Health 上传测试页上次同步时间 key,按 userId 隔离。
  26 + static String _appleHealthUploadTestLastSyncTimeKey(int userId) =>
  27 + 'apple_health_upload_test_last_sync_time_$userId';
  28 +
21 /// Onboarding 已全部完成的哨兵值 29 /// Onboarding 已全部完成的哨兵值
22 static const int _kOnboardingCompleted = -1; 30 static const int _kOnboardingCompleted = -1;
23 31
@@ -55,4 +63,30 @@ class UserAccountStorage { @@ -55,4 +63,30 @@ class UserAccountStorage {
55 /// 引导全部完成时调用。 63 /// 引导全部完成时调用。
56 Future<void> markOnboardingCompleted(int userId) => 64 Future<void> markOnboardingCompleted(int userId) =>
57 _prefs.setInt(_onboardingKey(userId), _kOnboardingCompleted); 65 _prefs.setInt(_onboardingKey(userId), _kOnboardingCompleted);
  66 +
  67 + // ─── Apple Health 上传记录 ────────────────────────────────────────────────
  68 +
  69 + String? appleHealthUploadLocalRecordJson(int userId) =>
  70 + _prefs.getString(_appleHealthUploadRecordKey(userId));
  71 +
  72 + Future<void> saveAppleHealthUploadLocalRecordJson(
  73 + int userId,
  74 + String value,
  75 + ) =>
  76 + _prefs.setString(_appleHealthUploadRecordKey(userId), value);
  77 +
  78 + Future<void> clearAppleHealthUploadLocalRecord(int userId) =>
  79 + _prefs.remove(_appleHealthUploadRecordKey(userId));
  80 +
  81 + int? appleHealthUploadTestLastSyncTime(int userId) =>
  82 + _prefs.getInt(_appleHealthUploadTestLastSyncTimeKey(userId));
  83 +
  84 + Future<void> saveAppleHealthUploadTestLastSyncTime(
  85 + int userId,
  86 + int latestSyncTime,
  87 + ) =>
  88 + _prefs.setInt(
  89 + _appleHealthUploadTestLastSyncTimeKey(userId),
  90 + latestSyncTime,
  91 + );
58 } 92 }
@@ -229,6 +229,53 @@ class HealthActivityTargetData { @@ -229,6 +229,53 @@ class HealthActivityTargetData {
229 ; 229 ;
230 } 230 }
231 231
  232 +class HealthAuthorization {
  233 + HealthAuthorization({
  234 + required this.status,
  235 + required this.hasData,
  236 + });
  237 +
  238 + /// -1: AppleHealth不可用, 0: 需要请求授权, 1: 已被授权且能获取到部分数据, 2: 已被拒绝或者没有数据
  239 + int status;
  240 +
  241 + bool hasData;
  242 +
  243 + List<Object?> _toList() {
  244 + return <Object?>[
  245 + status,
  246 + hasData,
  247 + ];
  248 + }
  249 +
  250 + Object encode() {
  251 + return _toList(); }
  252 +
  253 + static HealthAuthorization decode(Object result) {
  254 + result as List<Object?>;
  255 + return HealthAuthorization(
  256 + status: result[0]! as int,
  257 + hasData: result[1]! as bool,
  258 + );
  259 + }
  260 +
  261 + @override
  262 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  263 + bool operator ==(Object other) {
  264 + if (other is! HealthAuthorization || other.runtimeType != runtimeType) {
  265 + return false;
  266 + }
  267 + if (identical(this, other)) {
  268 + return true;
  269 + }
  270 + return _deepEquals(encode(), other.encode());
  271 + }
  272 +
  273 + @override
  274 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  275 + int get hashCode => Object.hashAll(_toList())
  276 +;
  277 +}
  278 +
232 279
233 class _PigeonCodec extends StandardMessageCodec { 280 class _PigeonCodec extends StandardMessageCodec {
234 const _PigeonCodec(); 281 const _PigeonCodec();
@@ -249,6 +296,9 @@ class _PigeonCodec extends StandardMessageCodec { @@ -249,6 +296,9 @@ class _PigeonCodec extends StandardMessageCodec {
249 } else if (value is HealthActivityTargetData) { 296 } else if (value is HealthActivityTargetData) {
250 buffer.putUint8(132); 297 buffer.putUint8(132);
251 writeValue(buffer, value.encode()); 298 writeValue(buffer, value.encode());
  299 + } else if (value is HealthAuthorization) {
  300 + buffer.putUint8(133);
  301 + writeValue(buffer, value.encode());
252 } else { 302 } else {
253 super.writeValue(buffer, value); 303 super.writeValue(buffer, value);
254 } 304 }
@@ -265,6 +315,8 @@ class _PigeonCodec extends StandardMessageCodec { @@ -265,6 +315,8 @@ class _PigeonCodec extends StandardMessageCodec {
265 return HealthSleepUploadDataPoint.decode(readValue(buffer)!); 315 return HealthSleepUploadDataPoint.decode(readValue(buffer)!);
266 case 132: 316 case 132:
267 return HealthActivityTargetData.decode(readValue(buffer)!); 317 return HealthActivityTargetData.decode(readValue(buffer)!);
  318 + case 133:
  319 + return HealthAuthorization.decode(readValue(buffer)!);
268 default: 320 default:
269 return super.readValueOfType(type, buffer); 321 return super.readValueOfType(type, buffer);
270 } 322 }
@@ -284,8 +336,8 @@ class HealthKitHostApi { @@ -284,8 +336,8 @@ class HealthKitHostApi {
284 336
285 final String pigeonVar_messageChannelSuffix; 337 final String pigeonVar_messageChannelSuffix;
286 338
287 - Future<bool> checkHealthAppAuthorization() async {  
288 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix'; 339 + Future<String> getHealthServerAuthUrl() async {
  340 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$pigeonVar_messageChannelSuffix';
289 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 341 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
290 pigeonVar_channelName, 342 pigeonVar_channelName,
291 pigeonChannelCodec, 343 pigeonChannelCodec,
@@ -308,12 +360,12 @@ class HealthKitHostApi { @@ -308,12 +360,12 @@ class HealthKitHostApi {
308 message: 'Host platform returned null value for non-null return value.', 360 message: 'Host platform returned null value for non-null return value.',
309 ); 361 );
310 } else { 362 } else {
311 - return (pigeonVar_replyList[0] as bool?)!; 363 + return (pigeonVar_replyList[0] as String?)!;
312 } 364 }
313 } 365 }
314 366
315 - Future<String> getHealthServerAuthUrl() async {  
316 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$pigeonVar_messageChannelSuffix'; 367 + Future<bool> cancelHealthAppAuthorization() async {
  368 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$pigeonVar_messageChannelSuffix';
317 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 369 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
318 pigeonVar_channelName, 370 pigeonVar_channelName,
319 pigeonChannelCodec, 371 pigeonChannelCodec,
@@ -336,13 +388,13 @@ class HealthKitHostApi { @@ -336,13 +388,13 @@ class HealthKitHostApi {
336 message: 'Host platform returned null value for non-null return value.', 388 message: 'Host platform returned null value for non-null return value.',
337 ); 389 );
338 } else { 390 } else {
339 - return (pigeonVar_replyList[0] as String?)!; 391 + return (pigeonVar_replyList[0] as bool?)!;
340 } 392 }
341 } 393 }
342 394
343 - /// Opens Huawei Health client authorization UI. Returns whether user granted.  
344 - Future<bool> requestHealthClientAuthorization() async {  
345 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$pigeonVar_messageChannelSuffix'; 395 + /// Runs native health read and server upload pipeline.
  396 + Future<HealthUploadResult> performHealthUpload() async {
  397 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$pigeonVar_messageChannelSuffix';
346 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 398 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
347 pigeonVar_channelName, 399 pigeonVar_channelName,
348 pigeonChannelCodec, 400 pigeonChannelCodec,
@@ -365,12 +417,13 @@ class HealthKitHostApi { @@ -365,12 +417,13 @@ class HealthKitHostApi {
365 message: 'Host platform returned null value for non-null return value.', 417 message: 'Host platform returned null value for non-null return value.',
366 ); 418 );
367 } else { 419 } else {
368 - return (pigeonVar_replyList[0] as bool?)!; 420 + return (pigeonVar_replyList[0] as HealthUploadResult?)!;
369 } 421 }
370 } 422 }
371 423
372 - Future<bool> cancelHealthAppAuthorization() async {  
373 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$pigeonVar_messageChannelSuffix'; 424 + /// Opens Huawei Health client authorization UI. Returns whether user granted.
  425 + Future<HealthAuthorization> checkHealthAppAuthorization() async {
  426 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix';
374 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 427 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
375 pigeonVar_channelName, 428 pigeonVar_channelName,
376 pigeonChannelCodec, 429 pigeonChannelCodec,
@@ -393,13 +446,12 @@ class HealthKitHostApi { @@ -393,13 +446,12 @@ class HealthKitHostApi {
393 message: 'Host platform returned null value for non-null return value.', 446 message: 'Host platform returned null value for non-null return value.',
394 ); 447 );
395 } else { 448 } else {
396 - return (pigeonVar_replyList[0] as bool?)!; 449 + return (pigeonVar_replyList[0] as HealthAuthorization?)!;
397 } 450 }
398 } 451 }
399 452
400 - /// Runs native health read and server upload pipeline.  
401 - Future<HealthUploadResult> performHealthUpload() async {  
402 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$pigeonVar_messageChannelSuffix'; 453 + Future<bool> requestHealthClientAuthorization() async {
  454 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$pigeonVar_messageChannelSuffix';
403 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 455 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
404 pigeonVar_channelName, 456 pigeonVar_channelName,
405 pigeonChannelCodec, 457 pigeonChannelCodec,
@@ -422,7 +474,7 @@ class HealthKitHostApi { @@ -422,7 +474,7 @@ class HealthKitHostApi {
422 message: 'Host platform returned null value for non-null return value.', 474 message: 'Host platform returned null value for non-null return value.',
423 ); 475 );
424 } else { 476 } else {
425 - return (pigeonVar_replyList[0] as HealthUploadResult?)!; 477 + return (pigeonVar_replyList[0] as bool?)!;
426 } 478 }
427 } 479 }
428 480
@@ -30,6 +30,14 @@ bool _deepEquals(Object? a, Object? b) { @@ -30,6 +30,14 @@ bool _deepEquals(Object? a, Object? b) {
30 } 30 }
31 31
32 32
  33 +enum AppleProductPaymentErrorMsg {
  34 + missingUUID,
  35 + productNotFound,
  36 + userCancelled,
  37 + failedVerification,
  38 + unknown,
  39 +}
  40 +
33 class AppleSignInModel { 41 class AppleSignInModel {
34 AppleSignInModel({ 42 AppleSignInModel({
35 required this.userId, 43 required this.userId,
@@ -86,6 +94,141 @@ class AppleSignInModel { @@ -86,6 +94,141 @@ class AppleSignInModel {
86 ; 94 ;
87 } 95 }
88 96
  97 +class AppleProductInfo {
  98 + AppleProductInfo({
  99 + required this.productId,
  100 + required this.originPriceDescription,
  101 + this.priceDescription,
  102 + required this.originPrice,
  103 + this.price,
  104 + required this.isTrialPeriod,
  105 + });
  106 +
  107 + String productId;
  108 +
  109 + String originPriceDescription;
  110 +
  111 + String? priceDescription;
  112 +
  113 + double originPrice;
  114 +
  115 + double? price;
  116 +
  117 + bool isTrialPeriod;
  118 +
  119 + List<Object?> _toList() {
  120 + return <Object?>[
  121 + productId,
  122 + originPriceDescription,
  123 + priceDescription,
  124 + originPrice,
  125 + price,
  126 + isTrialPeriod,
  127 + ];
  128 + }
  129 +
  130 + Object encode() {
  131 + return _toList(); }
  132 +
  133 + static AppleProductInfo decode(Object result) {
  134 + result as List<Object?>;
  135 + return AppleProductInfo(
  136 + productId: result[0]! as String,
  137 + originPriceDescription: result[1]! as String,
  138 + priceDescription: result[2] as String?,
  139 + originPrice: result[3]! as double,
  140 + price: result[4] as double?,
  141 + isTrialPeriod: result[5]! as bool,
  142 + );
  143 + }
  144 +
  145 + @override
  146 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  147 + bool operator ==(Object other) {
  148 + if (other is! AppleProductInfo || other.runtimeType != runtimeType) {
  149 + return false;
  150 + }
  151 + if (identical(this, other)) {
  152 + return true;
  153 + }
  154 + return _deepEquals(encode(), other.encode());
  155 + }
  156 +
  157 + @override
  158 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  159 + int get hashCode => Object.hashAll(_toList())
  160 +;
  161 +}
  162 +
  163 +class AppleProductPaymentResult {
  164 + AppleProductPaymentResult({
  165 + required this.productId,
  166 + this.appAccountToken,
  167 + this.originalTransactionId,
  168 + this.transactionId,
  169 + this.success,
  170 + this.errorMessage,
  171 + });
  172 +
  173 + String productId;
  174 +
  175 + String? appAccountToken;
  176 +
  177 + String? originalTransactionId;
  178 +
  179 + String? transactionId;
  180 +
  181 + bool? success;
  182 +
  183 + /// 错误描述:
  184 + /// 和AppleProductPaymentErrorMsg匹配的flutter处理,
  185 + /// 不匹配的则是StoreKit 直接给的错误描述,直接展示即可
  186 + String? errorMessage;
  187 +
  188 + List<Object?> _toList() {
  189 + return <Object?>[
  190 + productId,
  191 + appAccountToken,
  192 + originalTransactionId,
  193 + transactionId,
  194 + success,
  195 + errorMessage,
  196 + ];
  197 + }
  198 +
  199 + Object encode() {
  200 + return _toList(); }
  201 +
  202 + static AppleProductPaymentResult decode(Object result) {
  203 + result as List<Object?>;
  204 + return AppleProductPaymentResult(
  205 + productId: result[0]! as String,
  206 + appAccountToken: result[1] as String?,
  207 + originalTransactionId: result[2] as String?,
  208 + transactionId: result[3] as String?,
  209 + success: result[4] as bool?,
  210 + errorMessage: result[5] as String?,
  211 + );
  212 + }
  213 +
  214 + @override
  215 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  216 + bool operator ==(Object other) {
  217 + if (other is! AppleProductPaymentResult || other.runtimeType != runtimeType) {
  218 + return false;
  219 + }
  220 + if (identical(this, other)) {
  221 + return true;
  222 + }
  223 + return _deepEquals(encode(), other.encode());
  224 + }
  225 +
  226 + @override
  227 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  228 + int get hashCode => Object.hashAll(_toList())
  229 +;
  230 +}
  231 +
89 232
90 class _PigeonCodec extends StandardMessageCodec { 233 class _PigeonCodec extends StandardMessageCodec {
91 const _PigeonCodec(); 234 const _PigeonCodec();
@@ -94,8 +237,17 @@ class _PigeonCodec extends StandardMessageCodec { @@ -94,8 +237,17 @@ class _PigeonCodec extends StandardMessageCodec {
94 if (value is int) { 237 if (value is int) {
95 buffer.putUint8(4); 238 buffer.putUint8(4);
96 buffer.putInt64(value); 239 buffer.putInt64(value);
97 - } else if (value is AppleSignInModel) { 240 + } else if (value is AppleProductPaymentErrorMsg) {
98 buffer.putUint8(129); 241 buffer.putUint8(129);
  242 + writeValue(buffer, value.index);
  243 + } else if (value is AppleSignInModel) {
  244 + buffer.putUint8(130);
  245 + writeValue(buffer, value.encode());
  246 + } else if (value is AppleProductInfo) {
  247 + buffer.putUint8(131);
  248 + writeValue(buffer, value.encode());
  249 + } else if (value is AppleProductPaymentResult) {
  250 + buffer.putUint8(132);
99 writeValue(buffer, value.encode()); 251 writeValue(buffer, value.encode());
100 } else { 252 } else {
101 super.writeValue(buffer, value); 253 super.writeValue(buffer, value);
@@ -106,7 +258,14 @@ class _PigeonCodec extends StandardMessageCodec { @@ -106,7 +258,14 @@ class _PigeonCodec extends StandardMessageCodec {
106 Object? readValueOfType(int type, ReadBuffer buffer) { 258 Object? readValueOfType(int type, ReadBuffer buffer) {
107 switch (type) { 259 switch (type) {
108 case 129: 260 case 129:
  261 + final int? value = readValue(buffer) as int?;
  262 + return value == null ? null : AppleProductPaymentErrorMsg.values[value];
  263 + case 130:
109 return AppleSignInModel.decode(readValue(buffer)!); 264 return AppleSignInModel.decode(readValue(buffer)!);
  265 + case 131:
  266 + return AppleProductInfo.decode(readValue(buffer)!);
  267 + case 132:
  268 + return AppleProductPaymentResult.decode(readValue(buffer)!);
110 default: 269 default:
111 return super.readValueOfType(type, buffer); 270 return super.readValueOfType(type, buffer);
112 } 271 }
@@ -156,6 +315,7 @@ class PlatformHostApi { @@ -156,6 +315,7 @@ class PlatformHostApi {
156 } 315 }
157 } 316 }
158 317
  318 + /// 请求苹果登录
159 Future<AppleSignInModel?> requestAppleSignIn() async { 319 Future<AppleSignInModel?> requestAppleSignIn() async {
160 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix'; 320 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix';
161 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 321 final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
@@ -178,4 +338,52 @@ class PlatformHostApi { @@ -178,4 +338,52 @@ class PlatformHostApi {
178 return (pigeonVar_replyList[0] as AppleSignInModel?); 338 return (pigeonVar_replyList[0] as AppleSignInModel?);
179 } 339 }
180 } 340 }
  341 +
  342 + /// 查询指定id的苹果商品
  343 + Future<AppleProductInfo?> requestAppleProductInfo(String productId) async {
  344 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleProductInfo$pigeonVar_messageChannelSuffix';
  345 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  346 + pigeonVar_channelName,
  347 + pigeonChannelCodec,
  348 + binaryMessenger: pigeonVar_binaryMessenger,
  349 + );
  350 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[productId]);
  351 + final List<Object?>? pigeonVar_replyList =
  352 + await pigeonVar_sendFuture as List<Object?>?;
  353 + if (pigeonVar_replyList == null) {
  354 + throw _createConnectionError(pigeonVar_channelName);
  355 + } else if (pigeonVar_replyList.length > 1) {
  356 + throw PlatformException(
  357 + code: pigeonVar_replyList[0]! as String,
  358 + message: pigeonVar_replyList[1] as String?,
  359 + details: pigeonVar_replyList[2],
  360 + );
  361 + } else {
  362 + return (pigeonVar_replyList[0] as AppleProductInfo?);
  363 + }
  364 + }
  365 +
  366 + /// 从服务器下单后请求苹果支付
  367 + Future<AppleProductPaymentResult?> performApplePayment(String productId, String uuid) async {
  368 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performApplePayment$pigeonVar_messageChannelSuffix';
  369 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  370 + pigeonVar_channelName,
  371 + pigeonChannelCodec,
  372 + binaryMessenger: pigeonVar_binaryMessenger,
  373 + );
  374 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[productId, uuid]);
  375 + final List<Object?>? pigeonVar_replyList =
  376 + await pigeonVar_sendFuture as List<Object?>?;
  377 + if (pigeonVar_replyList == null) {
  378 + throw _createConnectionError(pigeonVar_channelName);
  379 + } else if (pigeonVar_replyList.length > 1) {
  380 + throw PlatformException(
  381 + code: pigeonVar_replyList[0]! as String,
  382 + message: pigeonVar_replyList[1] as String?,
  383 + details: pigeonVar_replyList[2],
  384 + );
  385 + } else {
  386 + return (pigeonVar_replyList[0] as AppleProductPaymentResult?);
  387 + }
  388 + }
181 } 389 }
@@ -46,6 +46,14 @@ class HealthActivityTargetData { @@ -46,6 +46,14 @@ class HealthActivityTargetData {
46 int? stand; 46 int? stand;
47 } 47 }
48 48
  49 +class HealthAuthorization {
  50 + /// -1: AppleHealth不可用, 0: 需要请求授权, 1: 已被授权且能获取到部分数据, 2: 已被拒绝或者没有数据
  51 + final int status;
  52 + final bool hasData;
  53 +
  54 + HealthAuthorization({required this.status, required this.hasData});
  55 +}
  56 +
49 @ConfigurePigeon( 57 @ConfigurePigeon(
50 PigeonOptions( 58 PigeonOptions(
51 dartOut: 'lib/pigeon/health_kit_api.g.dart', 59 dartOut: 'lib/pigeon/health_kit_api.g.dart',
@@ -64,87 +72,92 @@ class HealthActivityTargetData { @@ -64,87 +72,92 @@ class HealthActivityTargetData {
64 ) 72 )
65 @HostApi() 73 @HostApi()
66 abstract class HealthKitHostApi { 74 abstract class HealthKitHostApi {
67 - @async  
68 - bool checkHealthAppAuthorization(); 75 + // @optional
69 76
70 -@async 77 + @async
71 String getHealthServerAuthUrl(); 78 String getHealthServerAuthUrl();
72 79
73 - /// Opens Huawei Health client authorization UI. Returns whether user granted.  
74 - bool requestHealthClientAuthorization();  
75 -  
76 bool cancelHealthAppAuthorization(); 80 bool cancelHealthAppAuthorization();
77 81
78 /// Runs native health read and server upload pipeline. 82 /// Runs native health read and server upload pipeline.
79 HealthUploadResult performHealthUpload(); 83 HealthUploadResult performHealthUpload();
80 84
81 -@async 85 +// @required
  86 +
  87 + /// Opens Huawei Health client authorization UI. Returns whether user granted.
  88 + @async
  89 + HealthAuthorization checkHealthAppAuthorization();
  90 +
  91 + @async
  92 + bool requestHealthClientAuthorization();
  93 +
  94 + @async
82 List<HealthUploadDataPoint> fetchHrvData(int startTime, int endTime); 95 List<HealthUploadDataPoint> fetchHrvData(int startTime, int endTime);
83 96
84 -@async 97 + @async
85 List<HealthUploadDataPoint> fetchHeartRateData(int startTime, int endTime); 98 List<HealthUploadDataPoint> fetchHeartRateData(int startTime, int endTime);
86 99
87 -@async 100 + @async
88 List<HealthUploadDataPoint> fetchWalkingHeartRateData( 101 List<HealthUploadDataPoint> fetchWalkingHeartRateData(
89 int startTime, 102 int startTime,
90 int endTime, 103 int endTime,
91 ); 104 );
92 105
93 -@async 106 + @async
94 List<HealthUploadDataPoint> fetchRestingHeartRateData( 107 List<HealthUploadDataPoint> fetchRestingHeartRateData(
95 int startTime, 108 int startTime,
96 int endTime, 109 int endTime,
97 ); 110 );
98 111
99 -@async 112 + @async
100 List<HealthUploadDataPoint> fetchSleepingHeartRateData( 113 List<HealthUploadDataPoint> fetchSleepingHeartRateData(
101 int startTime, 114 int startTime,
102 int endTime, 115 int endTime,
103 ); 116 );
104 117
105 -@async 118 + @async
106 List<HealthUploadDataPoint> fetchOxygenSaturationData( 119 List<HealthUploadDataPoint> fetchOxygenSaturationData(
107 int startTime, 120 int startTime,
108 int endTime, 121 int endTime,
109 ); 122 );
110 123
111 -@async 124 + @async
112 List<HealthUploadDataPoint> fetchActiveEnergyData( 125 List<HealthUploadDataPoint> fetchActiveEnergyData(
113 int startTime, 126 int startTime,
114 int endTime, 127 int endTime,
115 ); 128 );
116 129
117 -@async 130 + @async
118 List<HealthUploadDataPoint> fetchExerciseData(int startTime, int endTime); 131 List<HealthUploadDataPoint> fetchExerciseData(int startTime, int endTime);
119 132
120 -@async 133 + @async
121 List<HealthUploadDataPoint> fetchStandData(int startTime, int endTime); 134 List<HealthUploadDataPoint> fetchStandData(int startTime, int endTime);
122 135
123 -@async 136 + @async
124 List<HealthUploadDataPoint> fetchStepCountData(int startTime, int endTime); 137 List<HealthUploadDataPoint> fetchStepCountData(int startTime, int endTime);
125 138
126 -@async 139 + @async
127 List<HealthSleepUploadDataPoint> fetchSleepData(int startTime, int endTime); 140 List<HealthSleepUploadDataPoint> fetchSleepData(int startTime, int endTime);
128 141
129 -@async 142 + @async
130 List<HealthUploadDataPoint> fetchSleepingWristTemperatureData( 143 List<HealthUploadDataPoint> fetchSleepingWristTemperatureData(
131 int startTime, 144 int startTime,
132 int endTime, 145 int endTime,
133 ); 146 );
134 147
135 -@async 148 + @async
136 List<HealthUploadDataPoint> fetchRespiratoryRateData( 149 List<HealthUploadDataPoint> fetchRespiratoryRateData(
137 int startTime, 150 int startTime,
138 int endTime, 151 int endTime,
139 ); 152 );
140 153
141 -@async 154 + @async
142 List<HealthUploadDataPoint> fetchIrregularHeartRhythmData( 155 List<HealthUploadDataPoint> fetchIrregularHeartRhythmData(
143 int startTime, 156 int startTime,
144 int endTime, 157 int endTime,
145 ); 158 );
146 159
147 -@async 160 + @async
148 HealthActivityTargetData? fetchActivityTargetData( 161 HealthActivityTargetData? fetchActivityTargetData(
149 int startTime, 162 int startTime,
150 int endTime, 163 int endTime,
@@ -14,6 +14,53 @@ class AppleSignInModel { @@ -14,6 +14,53 @@ class AppleSignInModel {
14 }); 14 });
15 } 15 }
16 16
  17 +class AppleProductInfo {
  18 + final String productId;
  19 + final String originPriceDescription;
  20 + final String? priceDescription;
  21 + final double originPrice;
  22 + final double? price;
  23 + final bool isTrialPeriod;
  24 + AppleProductInfo({
  25 + required this.productId,
  26 + required this.originPriceDescription,
  27 + this.priceDescription,
  28 + required this.originPrice,
  29 + this.price,
  30 + required this.isTrialPeriod,
  31 + });
  32 +}
  33 +
  34 +enum AppleProductPaymentErrorMsg {
  35 + missingUUID,
  36 + productNotFound,
  37 + userCancelled,
  38 + failedVerification,
  39 + unknown,
  40 +}
  41 +
  42 +class AppleProductPaymentResult {
  43 + final String productId;
  44 + final String? appAccountToken;
  45 + final String? originalTransactionId;
  46 + final String? transactionId;
  47 + final bool? success;
  48 +
  49 + /// 错误描述:
  50 + /// 和AppleProductPaymentErrorMsg匹配的flutter处理,
  51 + /// 不匹配的则是StoreKit 直接给的错误描述,直接展示即可
  52 + final String? errorMessage;
  53 +
  54 + AppleProductPaymentResult({
  55 + required this.productId,
  56 + this.appAccountToken,
  57 + this.originalTransactionId,
  58 + this.transactionId,
  59 + this.success,
  60 + this.errorMessage,
  61 + });
  62 +}
  63 +
17 @ConfigurePigeon( 64 @ConfigurePigeon(
18 PigeonOptions( 65 PigeonOptions(
19 dartOut: 'lib/pigeon/platform_api.g.dart', 66 dartOut: 'lib/pigeon/platform_api.g.dart',
@@ -36,6 +83,15 @@ abstract class PlatformHostApi { @@ -36,6 +83,15 @@ abstract class PlatformHostApi {
36 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` 83 /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
37 String getFullUserAgent(); 84 String getFullUserAgent();
38 85
39 -@async 86 + /// 请求苹果登录
  87 + @async
40 AppleSignInModel? requestAppleSignIn(); 88 AppleSignInModel? requestAppleSignIn();
  89 +
  90 + /// 查询指定id的苹果商品
  91 + @async
  92 + AppleProductInfo? requestAppleProductInfo(String productId);
  93 +
  94 + /// 从服务器下单后请求苹果支付
  95 + @async
  96 + AppleProductPaymentResult? performApplePayment(String productId, String uuid);
41 } 97 }
@@ -133,10 +133,10 @@ packages: @@ -133,10 +133,10 @@ packages:
133 dependency: transitive 133 dependency: transitive
134 description: 134 description:
135 name: characters 135 name: characters
136 - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 136 + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
137 url: "https://pub.dev" 137 url: "https://pub.dev"
138 source: hosted 138 source: hosted
139 - version: "1.4.0" 139 + version: "1.3.0"
140 checked_yaml: 140 checked_yaml:
141 dependency: transitive 141 dependency: transitive
142 description: 142 description:
@@ -149,10 +149,10 @@ packages: @@ -149,10 +149,10 @@ packages:
149 dependency: transitive 149 dependency: transitive
150 description: 150 description:
151 name: clock 151 name: clock
152 - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 152 + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
153 url: "https://pub.dev" 153 url: "https://pub.dev"
154 source: hosted 154 source: hosted
155 - version: "1.1.2" 155 + version: "1.1.1"
156 code_builder: 156 code_builder:
157 dependency: transitive 157 dependency: transitive
158 description: 158 description:
@@ -165,10 +165,10 @@ packages: @@ -165,10 +165,10 @@ packages:
165 dependency: transitive 165 dependency: transitive
166 description: 166 description:
167 name: collection 167 name: collection
168 - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 168 + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
169 url: "https://pub.dev" 169 url: "https://pub.dev"
170 source: hosted 170 source: hosted
171 - version: "1.19.1" 171 + version: "1.19.0"
172 convert: 172 convert:
173 dependency: transitive 173 dependency: transitive
174 description: 174 description:
@@ -237,10 +237,10 @@ packages: @@ -237,10 +237,10 @@ packages:
237 dependency: transitive 237 dependency: transitive
238 description: 238 description:
239 name: fake_async 239 name: fake_async
240 - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 240 + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
241 url: "https://pub.dev" 241 url: "https://pub.dev"
242 source: hosted 242 source: hosted
243 - version: "1.3.3" 243 + version: "1.3.1"
244 ffi: 244 ffi:
245 dependency: transitive 245 dependency: transitive
246 description: 246 description:
@@ -426,7 +426,7 @@ packages: @@ -426,7 +426,7 @@ packages:
426 dependency: transitive 426 dependency: transitive
427 description: 427 description:
428 path: image_cropper_for_web 428 path: image_cropper_for_web
429 - ref: "65c2c99891882ea59732959a672f3d5993a837bb" 429 + ref: "br_v9.1.0_ohos"
430 resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" 430 resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb"
431 url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" 431 url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
432 source: git 432 source: git
@@ -435,7 +435,7 @@ packages: @@ -435,7 +435,7 @@ packages:
435 dependency: transitive 435 dependency: transitive
436 description: 436 description:
437 path: image_cropper_platform_interface 437 path: image_cropper_platform_interface
438 - ref: "65c2c99891882ea59732959a672f3d5993a837bb" 438 + ref: "br_v9.1.0_ohos"
439 resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" 439 resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb"
440 url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" 440 url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
441 source: git 441 source: git
@@ -517,10 +517,10 @@ packages: @@ -517,10 +517,10 @@ packages:
517 dependency: "direct main" 517 dependency: "direct main"
518 description: 518 description:
519 name: intl 519 name: intl
520 - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" 520 + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
521 url: "https://pub.dev" 521 url: "https://pub.dev"
522 source: hosted 522 source: hosted
523 - version: "0.20.2" 523 + version: "0.19.0"
524 io: 524 io:
525 dependency: transitive 525 dependency: transitive
526 description: 526 description:
@@ -549,26 +549,26 @@ packages: @@ -549,26 +549,26 @@ packages:
549 dependency: transitive 549 dependency: transitive
550 description: 550 description:
551 name: leak_tracker 551 name: leak_tracker
552 - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 552 + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
553 url: "https://pub.dev" 553 url: "https://pub.dev"
554 source: hosted 554 source: hosted
555 - version: "11.0.2" 555 + version: "10.0.7"
556 leak_tracker_flutter_testing: 556 leak_tracker_flutter_testing:
557 dependency: transitive 557 dependency: transitive
558 description: 558 description:
559 name: leak_tracker_flutter_testing 559 name: leak_tracker_flutter_testing
560 - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 560 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
561 url: "https://pub.dev" 561 url: "https://pub.dev"
562 source: hosted 562 source: hosted
563 - version: "3.0.10" 563 + version: "3.0.8"
564 leak_tracker_testing: 564 leak_tracker_testing:
565 dependency: transitive 565 dependency: transitive
566 description: 566 description:
567 name: leak_tracker_testing 567 name: leak_tracker_testing
568 - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 568 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
569 url: "https://pub.dev" 569 url: "https://pub.dev"
570 source: hosted 570 source: hosted
571 - version: "3.0.2" 571 + version: "3.0.1"
572 lints: 572 lints:
573 dependency: transitive 573 dependency: transitive
574 description: 574 description:
@@ -597,10 +597,10 @@ packages: @@ -597,10 +597,10 @@ packages:
597 dependency: transitive 597 dependency: transitive
598 description: 598 description:
599 name: matcher 599 name: matcher
600 - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 600 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
601 url: "https://pub.dev" 601 url: "https://pub.dev"
602 source: hosted 602 source: hosted
603 - version: "0.12.17" 603 + version: "0.12.16+1"
604 material_color_utilities: 604 material_color_utilities:
605 dependency: transitive 605 dependency: transitive
606 description: 606 description:
@@ -613,10 +613,10 @@ packages: @@ -613,10 +613,10 @@ packages:
613 dependency: transitive 613 dependency: transitive
614 description: 614 description:
615 name: meta 615 name: meta
616 - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" 616 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
617 url: "https://pub.dev" 617 url: "https://pub.dev"
618 source: hosted 618 source: hosted
619 - version: "1.17.0" 619 + version: "1.15.0"
620 mime: 620 mime:
621 dependency: transitive 621 dependency: transitive
622 description: 622 description:
@@ -645,10 +645,10 @@ packages: @@ -645,10 +645,10 @@ packages:
645 dependency: transitive 645 dependency: transitive
646 description: 646 description:
647 name: path 647 name: path
648 - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 648 + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
649 url: "https://pub.dev" 649 url: "https://pub.dev"
650 source: hosted 650 source: hosted
651 - version: "1.9.1" 651 + version: "1.9.0"
652 path_provider: 652 path_provider:
653 dependency: transitive 653 dependency: transitive
654 description: 654 description:
@@ -966,18 +966,18 @@ packages: @@ -966,18 +966,18 @@ packages:
966 dependency: transitive 966 dependency: transitive
967 description: 967 description:
968 name: stack_trace 968 name: stack_trace
969 - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 969 + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
970 url: "https://pub.dev" 970 url: "https://pub.dev"
971 source: hosted 971 source: hosted
972 - version: "1.12.1" 972 + version: "1.12.0"
973 stream_channel: 973 stream_channel:
974 dependency: transitive 974 dependency: transitive
975 description: 975 description:
976 name: stream_channel 976 name: stream_channel
977 - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 977 + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
978 url: "https://pub.dev" 978 url: "https://pub.dev"
979 source: hosted 979 source: hosted
980 - version: "2.1.4" 980 + version: "2.1.2"
981 stream_transform: 981 stream_transform:
982 dependency: transitive 982 dependency: transitive
983 description: 983 description:
@@ -1006,10 +1006,10 @@ packages: @@ -1006,10 +1006,10 @@ packages:
1006 dependency: "direct main" 1006 dependency: "direct main"
1007 description: 1007 description:
1008 name: table_calendar 1008 name: table_calendar
1009 - sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" 1009 + sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
1010 url: "https://pub.dev" 1010 url: "https://pub.dev"
1011 source: hosted 1011 source: hosted
1012 - version: "3.2.0" 1012 + version: "3.1.3"
1013 term_glyph: 1013 term_glyph:
1014 dependency: transitive 1014 dependency: transitive
1015 description: 1015 description:
@@ -1022,10 +1022,10 @@ packages: @@ -1022,10 +1022,10 @@ packages:
1022 dependency: transitive 1022 dependency: transitive
1023 description: 1023 description:
1024 name: test_api 1024 name: test_api
1025 - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 1025 + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
1026 url: "https://pub.dev" 1026 url: "https://pub.dev"
1027 source: hosted 1027 source: hosted
1028 - version: "0.7.7" 1028 + version: "0.7.3"
1029 timing: 1029 timing:
1030 dependency: transitive 1030 dependency: transitive
1031 description: 1031 description:
@@ -1054,10 +1054,10 @@ packages: @@ -1054,10 +1054,10 @@ packages:
1054 dependency: transitive 1054 dependency: transitive
1055 description: 1055 description:
1056 name: vector_math 1056 name: vector_math
1057 - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 1057 + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
1058 url: "https://pub.dev" 1058 url: "https://pub.dev"
1059 source: hosted 1059 source: hosted
1060 - version: "2.2.0" 1060 + version: "2.1.4"
1061 vm_service: 1061 vm_service:
1062 dependency: transitive 1062 dependency: transitive
1063 description: 1063 description:
@@ -1111,8 +1111,8 @@ packages: @@ -1111,8 +1111,8 @@ packages:
1111 dependency: transitive 1111 dependency: transitive
1112 description: 1112 description:
1113 path: "packages/webview_flutter/webview_flutter_android" 1113 path: "packages/webview_flutter/webview_flutter_android"
1114 - ref: de942e79c9057b32ad31106508bd87c0d60aef83  
1115 - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 1114 + ref: "br_webview_flutter-v4.13.0_ohos"
  1115 + resolved-ref: ecd3d0b6195945ee0660c324d45f1acbc6198cc3
1116 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" 1116 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
1117 source: git 1117 source: git
1118 version: "4.7.0" 1118 version: "4.7.0"
@@ -1120,8 +1120,8 @@ packages: @@ -1120,8 +1120,8 @@ packages:
1120 dependency: transitive 1120 dependency: transitive
1121 description: 1121 description:
1122 path: "packages/webview_flutter/webview_flutter_ohos" 1122 path: "packages/webview_flutter/webview_flutter_ohos"
1123 - ref: de942e79c9057b32ad31106508bd87c0d60aef83  
1124 - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 1123 + ref: "br_webview_flutter-v4.13.0_ohos"
  1124 + resolved-ref: ecd3d0b6195945ee0660c324d45f1acbc6198cc3
1125 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" 1125 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
1126 source: git 1126 source: git
1127 version: "4.7.0" 1127 version: "4.7.0"
@@ -1129,8 +1129,8 @@ packages: @@ -1129,8 +1129,8 @@ packages:
1129 dependency: transitive 1129 dependency: transitive
1130 description: 1130 description:
1131 path: "packages/webview_flutter/webview_flutter_platform_interface" 1131 path: "packages/webview_flutter/webview_flutter_platform_interface"
1132 - ref: de942e79c9057b32ad31106508bd87c0d60aef83  
1133 - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 1132 + ref: "br_webview_flutter-v4.13.0_ohos"
  1133 + resolved-ref: ecd3d0b6195945ee0660c324d45f1acbc6198cc3
1134 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" 1134 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
1135 source: git 1135 source: git
1136 version: "2.13.1" 1136 version: "2.13.1"
@@ -1138,8 +1138,8 @@ packages: @@ -1138,8 +1138,8 @@ packages:
1138 dependency: transitive 1138 dependency: transitive
1139 description: 1139 description:
1140 path: "packages/webview_flutter/webview_flutter_wkwebview" 1140 path: "packages/webview_flutter/webview_flutter_wkwebview"
1141 - ref: de942e79c9057b32ad31106508bd87c0d60aef83  
1142 - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 1141 + ref: "br_webview_flutter-v4.13.0_ohos"
  1142 + resolved-ref: ecd3d0b6195945ee0660c324d45f1acbc6198cc3
1143 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" 1143 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
1144 source: git 1144 source: git
1145 version: "3.22.0" 1145 version: "3.22.0"
@@ -1160,5 +1160,5 @@ packages: @@ -1160,5 +1160,5 @@ packages:
1160 source: hosted 1160 source: hosted
1161 version: "3.1.3" 1161 version: "3.1.3"
1162 sdks: 1162 sdks:
1163 - dart: ">=3.8.0-0 <4.0.0" 1163 + dart: ">=3.6.2 <4.0.0"
1164 flutter: ">=3.27.0" 1164 flutter: ">=3.27.0"