Showing
29 changed files
with
1939 additions
and
240 deletions
| @@ -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 |
ios/Runner/ApplePayment.swift
0 → 100644
| 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 | +} |
ios/Runner/DebugLogger.swift
0 → 100644
| 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" |
-
Please register or login to post a comment