Showing
10 changed files
with
428 additions
and
93 deletions
| @@ -81,4 +81,7 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl | @@ -81,4 +81,7 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl | ||
| 81 | callback: (Result<AppleProductPaymentResult?>) -> Unit | 81 | callback: (Result<AppleProductPaymentResult?>) -> Unit |
| 82 | ) { | 82 | ) { |
| 83 | } | 83 | } |
| 84 | + | ||
| 85 | + override fun performRestore(callback: (Result<Boolean>) -> Unit) { | ||
| 86 | + } | ||
| 84 | } | 87 | } |
| @@ -6,15 +6,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | @@ -6,15 +6,16 @@ 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'; |
| 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 9 | -import 'package:doublefeel_flutter/core/error/app_error.dart'; | ||
| 10 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; | 9 | import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; |
| 11 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 10 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 12 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 11 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 13 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; | 12 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; |
| 14 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; | 13 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; |
| 14 | +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | ||
| 15 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 15 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| 16 | import 'package:flutter/material.dart'; | 16 | import 'package:flutter/material.dart'; |
| 17 | import 'package:get/get.dart'; | 17 | import 'package:get/get.dart'; |
| 18 | +import 'package:intl/intl.dart'; | ||
| 18 | 19 | ||
| 19 | /// HRV 趋势数据点 | 20 | /// HRV 趋势数据点 |
| 20 | class HrvDataPoint { | 21 | class HrvDataPoint { |
| @@ -115,6 +116,12 @@ class TodayController extends GetxController { | @@ -115,6 +116,12 @@ class TodayController extends GetxController { | ||
| 115 | 116 | ||
| 116 | final hrvAnnotations = <HrvAnnotation>[].obs; | 117 | final hrvAnnotations = <HrvAnnotation>[].obs; |
| 117 | 118 | ||
| 119 | + final v2HealthData = Rxn<V2HealthData>(); | ||
| 120 | + final v2StressScore = Rxn<V2StressScore>(); | ||
| 121 | + final v2LatestHrv = Rxn<V2LatestHrvData>(); | ||
| 122 | + final v2HrvTrend = Rxn<V2HrvTrendData>(); | ||
| 123 | + final v2RealtimeStress = Rxn<V2RealtimeStressData>(); | ||
| 124 | + | ||
| 118 | Future<void> requestHealthAuthorization() async { | 125 | Future<void> requestHealthAuthorization() async { |
| 119 | if (GetPlatform.isIOS) { | 126 | if (GetPlatform.isIOS) { |
| 120 | try { | 127 | try { |
| @@ -211,89 +218,44 @@ class TodayController extends GetxController { | @@ -211,89 +218,44 @@ class TodayController extends GetxController { | ||
| 211 | } | 218 | } |
| 212 | 219 | ||
| 213 | Future<void> _refreshHealthDataForDate(DateTime date) async { | 220 | Future<void> _refreshHealthDataForDate(DateTime date) async { |
| 214 | - final startYear = date.year; | ||
| 215 | - final startMonth = date.month; | ||
| 216 | - final startDate = startYear * 10000 + startMonth * 100 + 1; // e.g. 20260601 | ||
| 217 | - const dateRangeType = 1; | 221 | + _clearRealTimeData(); |
| 222 | + final intDate = int.parse(DateFormat('yyyyMM01').format(date)); | ||
| 218 | 223 | ||
| 219 | final isToday = DateUtils.isSameDay(date, lastSelectableDay); | 224 | final isToday = DateUtils.isSameDay(date, lastSelectableDay); |
| 220 | stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态'; | 225 | stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态'; |
| 221 | - | ||
| 222 | - final todayResultFuture = isToday | ||
| 223 | - ? _healthApi.getTodayData( | ||
| 224 | - isOther: isFriend, | ||
| 225 | - errorHandlingPolicy: null, | ||
| 226 | - ) | ||
| 227 | - : Future.value( | ||
| 228 | - AppFailure<TodayStatusData>(AppUnknownError('Not today'))); | ||
| 229 | - | ||
| 230 | - final latestHrvResultFuture = isToday && !isFriend | ||
| 231 | - ? _healthApi.getLatestHrvData( | ||
| 232 | - errorHandlingPolicy: null, | ||
| 233 | - ) | ||
| 234 | - : Future.value(AppFailure<LatestHrvData>(AppUnknownError('Not today'))); | ||
| 235 | - | ||
| 236 | - final hrvStatisticsResultFuture = _healthApi.getHrvStatistics( | ||
| 237 | - isOther: isFriend, | ||
| 238 | - dateRangeType: dateRangeType, | ||
| 239 | - startDate: startDate, | ||
| 240 | - targetUserId: friendUserId, | ||
| 241 | - errorHandlingPolicy: null, | ||
| 242 | - ); | ||
| 243 | - final sleepStatisticsResultFuture = _healthApi.getSleepStateStatistics( | ||
| 244 | - isOther: isFriend, | ||
| 245 | - dateRangeType: dateRangeType, | ||
| 246 | - startDate: startDate, | ||
| 247 | - targetUserId: friendUserId, | ||
| 248 | - errorHandlingPolicy: null, | ||
| 249 | - ); | ||
| 250 | - final activityStatisticsResultFuture = _healthApi.getActivityBurnStatistics( | ||
| 251 | - isOther: isFriend, | ||
| 252 | - dateRangeType: dateRangeType, | ||
| 253 | - startDate: startDate, | ||
| 254 | - targetUserId: friendUserId, | ||
| 255 | - errorHandlingPolicy: null, | ||
| 256 | - ); | ||
| 257 | - | ||
| 258 | - final todayResult = await todayResultFuture; | ||
| 259 | - final latestHrvResult = await latestHrvResultFuture; | ||
| 260 | - final hrvStatisticsResult = await hrvStatisticsResultFuture; | ||
| 261 | - final sleepStatisticsResult = await sleepStatisticsResultFuture; | ||
| 262 | - final activityStatisticsResult = await activityStatisticsResultFuture; | ||
| 263 | - | ||
| 264 | - if (selectedDate.value != date) return; | ||
| 265 | - | ||
| 266 | - if (!isToday) { | ||
| 267 | - _clearRealTimeData(); | ||
| 268 | - } | ||
| 269 | - | ||
| 270 | - if (todayResult case AppSuccess<TodayStatusData>(data: final today)) { | ||
| 271 | - _applyTodayData(today); | ||
| 272 | - } else if (todayResult case AppFailure(error: final error)) { | ||
| 273 | - if (isToday) { | ||
| 274 | - AppLogger.w('HealthApi.getTodayData failed: $error'); | ||
| 275 | - } | 226 | + switch (await _healthApi.getV2HealthData(friendUserId, intDate)) { |
| 227 | + case AppSuccess(:final data): | ||
| 228 | + v2HealthData.value = data; | ||
| 229 | + case AppFailure(): | ||
| 230 | + v2HealthData.value = null; | ||
| 276 | } | 231 | } |
| 277 | 232 | ||
| 278 | - if (latestHrvResult case AppSuccess<LatestHrvData>(data: final latestHrv)) { | ||
| 279 | - _applyLatestHrvData(latestHrv); | 233 | + switch (await _healthApi.getV2StressScore(friendUserId, intDate)) { |
| 234 | + case AppSuccess(:final data): | ||
| 235 | + v2StressScore.value = data; | ||
| 236 | + case AppFailure(): | ||
| 237 | + v2StressScore.value = null; | ||
| 280 | } | 238 | } |
| 281 | 239 | ||
| 282 | - if (hrvStatisticsResult | ||
| 283 | - case AppSuccess<HrvStatisticsData>(data: final hrvStatistics)) { | ||
| 284 | - _applyHrvStatistics(hrvStatistics, date); | 240 | + switch (await _healthApi.getV2LatestHrv(friendUserId, intDate)) { |
| 241 | + case AppSuccess(:final data): | ||
| 242 | + v2LatestHrv.value = data; | ||
| 243 | + case AppFailure(): | ||
| 244 | + v2LatestHrv.value = null; | ||
| 285 | } | 245 | } |
| 286 | 246 | ||
| 287 | - if (sleepStatisticsResult | ||
| 288 | - case AppSuccess<SleepStatisticsData>(data: final sleepStatistics)) { | ||
| 289 | - _applySleepStatistics(sleepStatistics, date); | 247 | + switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) { |
| 248 | + case AppSuccess(:final data): | ||
| 249 | + v2HrvTrend.value = data; | ||
| 250 | + case AppFailure(): | ||
| 251 | + v2HrvTrend.value = null; | ||
| 290 | } | 252 | } |
| 291 | 253 | ||
| 292 | - if (activityStatisticsResult | ||
| 293 | - case AppSuccess<ActivityBurnStatisticsData>( | ||
| 294 | - data: final activityStatistics | ||
| 295 | - )) { | ||
| 296 | - _applyActivityStatistics(activityStatistics, date); | 254 | + switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) { |
| 255 | + case AppSuccess(:final data): | ||
| 256 | + v2RealtimeStress.value = data; | ||
| 257 | + case AppFailure(): | ||
| 258 | + v2RealtimeStress.value = null; | ||
| 297 | } | 259 | } |
| 298 | } | 260 | } |
| 299 | 261 |
| @@ -477,7 +477,22 @@ class _LatestHrvCard extends StatelessWidget { | @@ -477,7 +477,22 @@ class _LatestHrvCard extends StatelessWidget { | ||
| 477 | final colors = context.colors; | 477 | final colors = context.colors; |
| 478 | 478 | ||
| 479 | return Obx(() { | 479 | return Obx(() { |
| 480 | - controller.avgHrv.value; | 480 | + final latestHrv = controller.v2LatestHrv.value?.latestHrv; |
| 481 | + final latestDataTime = controller.v2LatestHrv.value?.latestDataTime; | ||
| 482 | + final state = controller.v2LatestHrv.value?.state; | ||
| 483 | + final tip = controller.v2LatestHrv.value?.tip; | ||
| 484 | + | ||
| 485 | + if (latestHrv == null || | ||
| 486 | + latestDataTime == null || | ||
| 487 | + state == null || | ||
| 488 | + tip == null) { | ||
| 489 | + return const SizedBox.shrink(); | ||
| 490 | + } | ||
| 491 | + final latestHrvText = '$latestHrv ms'; | ||
| 492 | + final latestDataTimeText = | ||
| 493 | + '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(latestDataTime * 1000))}'; | ||
| 494 | + final stateText = | ||
| 495 | + '${state == 1 ? '状态优秀' : state == 2 ? '状态' : state == 3 ? '状态一般' : state == 4 ? '状态较差' : '状态极差'}'; | ||
| 481 | return Container( | 496 | return Container( |
| 482 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), | 497 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), |
| 483 | decoration: BoxDecoration( | 498 | decoration: BoxDecoration( |
| @@ -488,7 +503,7 @@ class _LatestHrvCard extends StatelessWidget { | @@ -488,7 +503,7 @@ class _LatestHrvCard extends StatelessWidget { | ||
| 488 | crossAxisAlignment: CrossAxisAlignment.start, | 503 | crossAxisAlignment: CrossAxisAlignment.start, |
| 489 | children: [ | 504 | children: [ |
| 490 | Text( | 505 | Text( |
| 491 | - '最近一次(17:20)', | 506 | + '最近一次($latestDataTimeText)', |
| 492 | style: TextStyle( | 507 | style: TextStyle( |
| 493 | color: colors.textSecondary, | 508 | color: colors.textSecondary, |
| 494 | fontSize: 12, | 509 | fontSize: 12, |
| @@ -508,7 +523,7 @@ class _LatestHrvCard extends StatelessWidget { | @@ -508,7 +523,7 @@ class _LatestHrvCard extends StatelessWidget { | ||
| 508 | ), | 523 | ), |
| 509 | const SizedBox(width: 8), | 524 | const SizedBox(width: 8), |
| 510 | Text( | 525 | Text( |
| 511 | - '66ms·状态优秀', | 526 | + '${latestHrvText}ms·$stateText', |
| 512 | style: TextStyle( | 527 | style: TextStyle( |
| 513 | color: colors.textPrimary, | 528 | color: colors.textPrimary, |
| 514 | fontSize: 18, | 529 | fontSize: 18, |
| @@ -519,7 +534,7 @@ class _LatestHrvCard extends StatelessWidget { | @@ -519,7 +534,7 @@ class _LatestHrvCard extends StatelessWidget { | ||
| 519 | ), | 534 | ), |
| 520 | const SizedBox(height: 8), | 535 | const SizedBox(height: 8), |
| 521 | Text( | 536 | Text( |
| 522 | - '你的 HRV 高于平时水平。你目前状态放松,压力控制良好——继续保持这样的平衡状态。', | 537 | + tip, |
| 523 | maxLines: 2, | 538 | maxLines: 2, |
| 524 | overflow: TextOverflow.ellipsis, | 539 | overflow: TextOverflow.ellipsis, |
| 525 | style: TextStyle( | 540 | style: TextStyle( |
| @@ -25,7 +25,8 @@ class TodayHrvNumberCard extends StatelessWidget { | @@ -25,7 +25,8 @@ class TodayHrvNumberCard extends StatelessWidget { | ||
| 25 | Expanded( | 25 | Expanded( |
| 26 | child: _NumberItem( | 26 | child: _NumberItem( |
| 27 | label: context.l10n.averageHrvForTheDay, | 27 | label: context.l10n.averageHrvForTheDay, |
| 28 | - value: controller.avgHrv.value, | 28 | + value: |
| 29 | + controller.v2HealthData.value?.hrvAvg?.toString() ?? '--', | ||
| 29 | unit: 'ms', | 30 | unit: 'ms', |
| 30 | ), | 31 | ), |
| 31 | ), | 32 | ), |
| @@ -33,7 +34,9 @@ class TodayHrvNumberCard extends StatelessWidget { | @@ -33,7 +34,9 @@ class TodayHrvNumberCard extends StatelessWidget { | ||
| 33 | Expanded( | 34 | Expanded( |
| 34 | child: _NumberItem( | 35 | child: _NumberItem( |
| 35 | label: context.l10n.restingHeartRate, | 36 | label: context.l10n.restingHeartRate, |
| 36 | - value: controller.restingHeartRate.value, | 37 | + value: controller.v2HealthData.value?.lastRestingHrValue |
| 38 | + ?.toString() ?? | ||
| 39 | + '--', | ||
| 37 | unit: 'bpm', | 40 | unit: 'bpm', |
| 38 | ), | 41 | ), |
| 39 | ), | 42 | ), |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | +import 'dart:convert'; | ||
| 3 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; | 4 | import 'package:doublefeel_flutter/core/constants/app_const.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | ||
| 6 | +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 3 | import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart'; | 7 | import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart'; |
| 4 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 8 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 5 | import 'package:flutter/widgets.dart'; | 9 | import 'package:flutter/widgets.dart'; |
| @@ -177,11 +181,7 @@ class LoginController extends GetxController { | @@ -177,11 +181,7 @@ class LoginController extends GetxController { | ||
| 177 | 181 | ||
| 178 | Future<void> _loginWithApple(AppleSignInModel appleModel) async { | 182 | Future<void> _loginWithApple(AppleSignInModel appleModel) async { |
| 179 | isLoggingIn.value = true; | 183 | isLoggingIn.value = true; |
| 180 | - final loginRes = await _userApi.loginWithApple( | ||
| 181 | - identityToken: appleModel.identityToken!, | ||
| 182 | - email: appleModel.email, | ||
| 183 | - nickname: appleModel.nickname, | ||
| 184 | - ); | 184 | + final loginRes = await _userApi.loginWithApple(appleModel); |
| 185 | 185 | ||
| 186 | if (loginRes is AppSuccess<LoginResponse>) { | 186 | if (loginRes is AppSuccess<LoginResponse>) { |
| 187 | await _handleLoginSuccess(loginRes.data, isApple: true); | 187 | await _handleLoginSuccess(loginRes.data, isApple: true); |
| @@ -246,6 +246,24 @@ class LoginController extends GetxController { | @@ -246,6 +246,24 @@ class LoginController extends GetxController { | ||
| 246 | vip: vip, | 246 | vip: vip, |
| 247 | ); | 247 | ); |
| 248 | 248 | ||
| 249 | + try { | ||
| 250 | + if (Get.isRegistered<AppEnvironmentConfig>()) { | ||
| 251 | + var env = Get.find<AppEnvironmentConfig>(); | ||
| 252 | + await PlatformHostApi().updateLoginInfo( | ||
| 253 | + jsonEncode(UserPreferences( | ||
| 254 | + accessToken: accessToken, | ||
| 255 | + meUserInfo: me, | ||
| 256 | + partnerUserInfo: partner, | ||
| 257 | + vipInfo: vip != null | ||
| 258 | + ? UserPreferencesVipInfo.fromVipInfo(vip) | ||
| 259 | + : null, | ||
| 260 | + )), | ||
| 261 | + env.serverBaseUrl); | ||
| 262 | + } | ||
| 263 | + } on Exception catch (e) { | ||
| 264 | + AppLogger.e(e); | ||
| 265 | + } | ||
| 266 | + | ||
| 249 | await _userStateService.onLogin(); | 267 | await _userStateService.onLogin(); |
| 250 | 268 | ||
| 251 | // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用 | 269 | // 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用 |
| @@ -4,6 +4,7 @@ import '../../result/safe_call.dart'; | @@ -4,6 +4,7 @@ import '../../result/safe_call.dart'; | ||
| 4 | import '../../../data/models/enums/app_enums.dart'; | 4 | import '../../../data/models/enums/app_enums.dart'; |
| 5 | import '../../../data/models/health/health_models.dart'; | 5 | import '../../../data/models/health/health_models.dart'; |
| 6 | import '../../../data/models/health/health_upload_models.dart'; | 6 | import '../../../data/models/health/health_upload_models.dart'; |
| 7 | +import '../../../data/models/health/health_v2_models.dart'; | ||
| 7 | import '../api_paths.dart'; | 8 | import '../api_paths.dart'; |
| 8 | import '../dio_client.dart'; | 9 | import '../dio_client.dart'; |
| 9 | 10 | ||
| @@ -248,4 +249,92 @@ class HealthApi { | @@ -248,4 +249,92 @@ class HealthApi { | ||
| 248 | errorHandlingPolicy: errorHandlingPolicy, | 249 | errorHandlingPolicy: errorHandlingPolicy, |
| 249 | ); | 250 | ); |
| 250 | } | 251 | } |
| 252 | + | ||
| 253 | + /// V2 | ||
| 254 | + | ||
| 255 | + Future<AppResult<V2LatestHrvData>> getV2LatestHrv( | ||
| 256 | + int? friendUserId, | ||
| 257 | + int intDate, | ||
| 258 | + ) { | ||
| 259 | + return safeCall( | ||
| 260 | + call: () async { | ||
| 261 | + final response = | ||
| 262 | + await _dioClient.dio.get(ApiPaths.v2LastestHrv, queryParameters: { | ||
| 263 | + if (friendUserId != null) 'query_user_id': friendUserId, | ||
| 264 | + 'date': intDate, | ||
| 265 | + }); | ||
| 266 | + return V2LatestHrvData.fromJson(response.data as Map<String, dynamic>); | ||
| 267 | + }, | ||
| 268 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 269 | + ); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + Future<AppResult<V2HealthData>> getV2HealthData( | ||
| 273 | + int? friendUserId, | ||
| 274 | + int intDate, | ||
| 275 | + ) { | ||
| 276 | + return safeCall( | ||
| 277 | + call: () async { | ||
| 278 | + final response = | ||
| 279 | + await _dioClient.dio.get(ApiPaths.v2LastestData, queryParameters: { | ||
| 280 | + if (friendUserId != null) 'query_user_id': friendUserId, | ||
| 281 | + 'date': intDate, | ||
| 282 | + }); | ||
| 283 | + return V2HealthData.fromJson(response.data as Map<String, dynamic>); | ||
| 284 | + }, | ||
| 285 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 286 | + ); | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + Future<AppResult<V2StressScore>> getV2StressScore( | ||
| 290 | + int? friendUserId, | ||
| 291 | + int intDate, | ||
| 292 | + ) { | ||
| 293 | + return safeCall( | ||
| 294 | + call: () async { | ||
| 295 | + final response = | ||
| 296 | + await _dioClient.dio.get(ApiPaths.v2StressScore, queryParameters: { | ||
| 297 | + if (friendUserId != null) 'query_user_id': friendUserId, | ||
| 298 | + 'date': intDate, | ||
| 299 | + }); | ||
| 300 | + return V2StressScore.fromJson(response.data as Map<String, dynamic>); | ||
| 301 | + }, | ||
| 302 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 303 | + ); | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + Future<AppResult<V2HrvTrendData>> getV2HrvTrend( | ||
| 307 | + int? friendUserId, | ||
| 308 | + int intDate, | ||
| 309 | + ) { | ||
| 310 | + return safeCall( | ||
| 311 | + call: () async { | ||
| 312 | + final response = | ||
| 313 | + await _dioClient.dio.get(ApiPaths.v2HrvTrend, queryParameters: { | ||
| 314 | + if (friendUserId != null) 'query_user_id': friendUserId, | ||
| 315 | + 'date': intDate, | ||
| 316 | + }); | ||
| 317 | + return V2HrvTrendData.fromJson(response.data as Map<String, dynamic>); | ||
| 318 | + }, | ||
| 319 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 320 | + ); | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + Future<AppResult<V2RealtimeStressData>> getV2RealtimeStress( | ||
| 324 | + int? friendUserId, | ||
| 325 | + int intDate, | ||
| 326 | + ) { | ||
| 327 | + return safeCall( | ||
| 328 | + call: () async { | ||
| 329 | + final response = await _dioClient.dio | ||
| 330 | + .get(ApiPaths.v2RealtimeStress, queryParameters: { | ||
| 331 | + if (friendUserId != null) 'query_user_id': friendUserId, | ||
| 332 | + 'date': intDate, | ||
| 333 | + }); | ||
| 334 | + return V2RealtimeStressData.fromJson( | ||
| 335 | + response.data as Map<String, dynamic>); | ||
| 336 | + }, | ||
| 337 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 338 | + ); | ||
| 339 | + } | ||
| 251 | } | 340 | } |
| 1 | +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | ||
| 2 | + | ||
| 1 | import '../../constants/app_const.dart'; | 3 | import '../../constants/app_const.dart'; |
| 2 | import '../../error/http_error_handling_policy.dart'; | 4 | import '../../error/http_error_handling_policy.dart'; |
| 3 | import '../../result/app_result.dart'; | 5 | import '../../result/app_result.dart'; |
| @@ -42,20 +44,17 @@ class UserApi { | @@ -42,20 +44,17 @@ class UserApi { | ||
| 42 | ); | 44 | ); |
| 43 | } | 45 | } |
| 44 | 46 | ||
| 45 | - Future<AppResult<LoginResponse>> loginWithApple({ | ||
| 46 | - required String identityToken, | ||
| 47 | - String? email, | ||
| 48 | - String? nickname, | ||
| 49 | - }) { | 47 | + Future<AppResult<LoginResponse>> loginWithApple(AppleSignInModel appleModel) { |
| 50 | return safeCall( | 48 | return safeCall( |
| 51 | call: () async { | 49 | call: () async { |
| 52 | final response = await _dioClient.dio.post( | 50 | final response = await _dioClient.dio.post( |
| 53 | ApiPaths.userLogin, | 51 | ApiPaths.userLogin, |
| 54 | data: AppleLoginRequest( | 52 | data: AppleLoginRequest( |
| 55 | - identityToken: identityToken, | ||
| 56 | - email: email, | ||
| 57 | - nickname: nickname, | ||
| 58 | - ).toJson(), | 53 | + identityToken: appleModel.identityToken ?? '', |
| 54 | + email: appleModel.email ?? '', | ||
| 55 | + nickname: appleModel.nickname ?? '', | ||
| 56 | + userId: appleModel.userId) | ||
| 57 | + .toJson(), | ||
| 59 | options: _dioClient.noTokenOptions(), | 58 | options: _dioClient.noTokenOptions(), |
| 60 | ); | 59 | ); |
| 61 | return LoginResponse.fromJson(response.data as Map<String, dynamic>); | 60 | return LoginResponse.fromJson(response.data as Map<String, dynamic>); |
| @@ -29,6 +29,13 @@ abstract final class ApiPaths { | @@ -29,6 +29,13 @@ abstract final class ApiPaths { | ||
| 29 | static const healthStatsHrv = '/client/doublefeel/health/statistics/hrv/'; | 29 | static const healthStatsHrv = '/client/doublefeel/health/statistics/hrv/'; |
| 30 | static const healthPulse = '/client/doublefeel/health/pulse/'; | 30 | static const healthPulse = '/client/doublefeel/health/pulse/'; |
| 31 | static const healthPulseLatest = '/client/doublefeel/health/pulse/latest/'; | 31 | static const healthPulseLatest = '/client/doublefeel/health/pulse/latest/'; |
| 32 | + //Health v2 | ||
| 33 | + static const v2LastestHrv = '/client/doublefeel/health/v2/lastest_hrv/'; | ||
| 34 | + static const v2LastestData = '/client/doublefeel/health/v2/health_data/'; | ||
| 35 | + static const v2StressScore = '/client/doublefeel/health/v2/stress_score/'; | ||
| 36 | + static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/'; | ||
| 37 | + static const v2RealtimeStress = | ||
| 38 | + '/client/doublefeel/health/v2/realtime_stress/'; | ||
| 32 | 39 | ||
| 33 | // Pay | 40 | // Pay |
| 34 | static const paymentProducts = '/client/doublefeel/payment/products/'; | 41 | static const paymentProducts = '/client/doublefeel/payment/products/'; |
lib/data/models/health/health_v2_models.dart
0 → 100644
| 1 | +int? _parseInt(dynamic value) { | ||
| 2 | + if (value == null) return null; | ||
| 3 | + if (value is int) return value; | ||
| 4 | + if (value is double) return value.toInt(); | ||
| 5 | + if (value is String) return int.tryParse(value); | ||
| 6 | + return null; | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +double? _parseDouble(dynamic value) { | ||
| 10 | + if (value == null) return null; | ||
| 11 | + if (value is num) return value.toDouble(); | ||
| 12 | + if (value is String) { | ||
| 13 | + if (value == 'NaN') return double.nan; | ||
| 14 | + if (value == 'Infinity') return double.infinity; | ||
| 15 | + if (value == '-Infinity') return double.negativeInfinity; | ||
| 16 | + return double.tryParse(value); | ||
| 17 | + } | ||
| 18 | + return null; | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +/// v2/lastest_hrv/ response | ||
| 22 | +class V2LatestHrvData { | ||
| 23 | + const V2LatestHrvData( | ||
| 24 | + {this.latestHrv, this.latestDataTime, this.state, this.tip}); | ||
| 25 | + | ||
| 26 | + final double? latestHrv; | ||
| 27 | + final int? latestDataTime; | ||
| 28 | + final int? state; | ||
| 29 | + final String? tip; | ||
| 30 | + | ||
| 31 | + factory V2LatestHrvData.fromJson(Map<String, dynamic> json) { | ||
| 32 | + return V2LatestHrvData( | ||
| 33 | + latestHrv: _parseDouble(json['latest_hrv']), | ||
| 34 | + latestDataTime: _parseInt(json['latest_data_time']), | ||
| 35 | + state: _parseInt(json['state']), | ||
| 36 | + tip: json['tip_text'], | ||
| 37 | + ); | ||
| 38 | + } | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +/// v2/health_data/ response | ||
| 42 | +class V2HealthData { | ||
| 43 | + const V2HealthData({ | ||
| 44 | + this.hrvAvg, | ||
| 45 | + this.lastRestingHrValue, | ||
| 46 | + this.hrAvg, | ||
| 47 | + this.move, | ||
| 48 | + this.exercise, | ||
| 49 | + this.stand, | ||
| 50 | + this.steps, | ||
| 51 | + this.sleepDuration, | ||
| 52 | + this.sleepScore, | ||
| 53 | + this.sleepState, | ||
| 54 | + }); | ||
| 55 | + | ||
| 56 | + final int? hrvAvg; | ||
| 57 | + final int? lastRestingHrValue; | ||
| 58 | + final int? hrAvg; | ||
| 59 | + final int? move; | ||
| 60 | + final int? exercise; | ||
| 61 | + final int? stand; | ||
| 62 | + final int? steps; | ||
| 63 | + final int? sleepDuration; | ||
| 64 | + final double? sleepScore; | ||
| 65 | + final int? sleepState; | ||
| 66 | + | ||
| 67 | + factory V2HealthData.fromJson(Map<String, dynamic> json) { | ||
| 68 | + return V2HealthData( | ||
| 69 | + hrvAvg: _parseInt(json['hrv_avg']), | ||
| 70 | + lastRestingHrValue: _parseInt(json['last_resting_hr_value']), | ||
| 71 | + hrAvg: _parseInt(json['hr_avg']), | ||
| 72 | + move: _parseInt(json['move']), | ||
| 73 | + exercise: _parseInt(json['exercise']), | ||
| 74 | + stand: _parseInt(json['stand']), | ||
| 75 | + steps: _parseInt(json['steps']), | ||
| 76 | + sleepDuration: _parseInt(json['sleep_duration']), | ||
| 77 | + sleepScore: _parseDouble(json['sleep_score']), | ||
| 78 | + sleepState: _parseInt(json['sleep_state']), | ||
| 79 | + ); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + Map<String, dynamic> toJson() { | ||
| 83 | + final val = <String, dynamic>{}; | ||
| 84 | + if (hrvAvg != null) val['hrv_avg'] = hrvAvg; | ||
| 85 | + if (lastRestingHrValue != null) { | ||
| 86 | + val['last_resting_hr_value'] = lastRestingHrValue; | ||
| 87 | + } | ||
| 88 | + if (hrAvg != null) val['hr_avg'] = hrAvg; | ||
| 89 | + if (move != null) val['move'] = move; | ||
| 90 | + if (exercise != null) val['exercise'] = exercise; | ||
| 91 | + if (stand != null) val['stand'] = stand; | ||
| 92 | + if (steps != null) val['steps'] = steps; | ||
| 93 | + if (sleepDuration != null) val['sleep_duration'] = sleepDuration; | ||
| 94 | + if (sleepScore != null) val['sleep_score'] = sleepScore; | ||
| 95 | + if (sleepState != null) val['sleep_state'] = sleepState; | ||
| 96 | + return val; | ||
| 97 | + } | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +/// v2/stress_score/ response | ||
| 101 | +class V2StressScore { | ||
| 102 | + const V2StressScore({ | ||
| 103 | + this.state, | ||
| 104 | + this.hrvScore, | ||
| 105 | + this.hrScore, | ||
| 106 | + this.latestHr, | ||
| 107 | + this.comprehensiveScore, | ||
| 108 | + }); | ||
| 109 | + | ||
| 110 | + final int? state; | ||
| 111 | + final int? hrvScore; | ||
| 112 | + final int? hrScore; | ||
| 113 | + final int? latestHr; | ||
| 114 | + final int? comprehensiveScore; | ||
| 115 | + | ||
| 116 | + factory V2StressScore.fromJson(Map<String, dynamic> json) { | ||
| 117 | + return V2StressScore( | ||
| 118 | + state: _parseInt(json['state']), | ||
| 119 | + hrvScore: _parseInt(json['hrv_score']), | ||
| 120 | + hrScore: _parseInt(json['hr_score']), | ||
| 121 | + latestHr: _parseInt(json['latest_hr']), | ||
| 122 | + comprehensiveScore: _parseInt(json['comprehensive_score']), | ||
| 123 | + ); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + Map<String, dynamic> toJson() { | ||
| 127 | + final val = <String, dynamic>{}; | ||
| 128 | + if (state != null) val['state'] = state; | ||
| 129 | + if (hrvScore != null) val['hrv_score'] = hrvScore; | ||
| 130 | + if (hrScore != null) val['hr_score'] = hrScore; | ||
| 131 | + if (latestHr != null) val['latest_hr'] = latestHr; | ||
| 132 | + if (comprehensiveScore != null) { | ||
| 133 | + val['comprehensive_score'] = comprehensiveScore; | ||
| 134 | + } | ||
| 135 | + return val; | ||
| 136 | + } | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +/// Single item in v2/hrv_trend/ list | ||
| 140 | +class V2HrvTrendItem { | ||
| 141 | + const V2HrvTrendItem({this.time, this.rawHrv, this.trendHrv, this.state}); | ||
| 142 | + | ||
| 143 | + final int? time; | ||
| 144 | + final double? rawHrv; | ||
| 145 | + final int? trendHrv; | ||
| 146 | + final int? state; | ||
| 147 | + | ||
| 148 | + factory V2HrvTrendItem.fromJson(Map<String, dynamic> json) { | ||
| 149 | + return V2HrvTrendItem( | ||
| 150 | + time: _parseInt(json['time']), | ||
| 151 | + rawHrv: _parseDouble(json['raw_hrv']), | ||
| 152 | + trendHrv: _parseInt(json['trend_hrv']), | ||
| 153 | + state: _parseInt(json['state']), | ||
| 154 | + ); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + Map<String, dynamic> toJson() { | ||
| 158 | + final val = <String, dynamic>{}; | ||
| 159 | + if (time != null) val['time'] = time; | ||
| 160 | + if (rawHrv != null) val['raw_hrv'] = rawHrv; | ||
| 161 | + if (trendHrv != null) val['trend_hrv'] = trendHrv; | ||
| 162 | + if (state != null) val['state'] = state; | ||
| 163 | + return val; | ||
| 164 | + } | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +/// v2/hrv_trend/ response | ||
| 168 | +class V2HrvTrendData { | ||
| 169 | + const V2HrvTrendData({this.list}); | ||
| 170 | + | ||
| 171 | + final List<V2HrvTrendItem>? list; | ||
| 172 | + | ||
| 173 | + factory V2HrvTrendData.fromJson(Map<String, dynamic> json) { | ||
| 174 | + return V2HrvTrendData( | ||
| 175 | + list: (json['list'] as List<dynamic>?) | ||
| 176 | + ?.map((e) => V2HrvTrendItem.fromJson(e as Map<String, dynamic>)) | ||
| 177 | + .toList(), | ||
| 178 | + ); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + Map<String, dynamic> toJson() { | ||
| 182 | + final val = <String, dynamic>{}; | ||
| 183 | + if (list != null) { | ||
| 184 | + val['list'] = list!.map((e) => e.toJson()).toList(); | ||
| 185 | + } | ||
| 186 | + return val; | ||
| 187 | + } | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +/// Single item in v2/realtime_stress/ list | ||
| 191 | +class V2RealtimeStressItem { | ||
| 192 | + const V2RealtimeStressItem({this.time, this.value, this.state}); | ||
| 193 | + | ||
| 194 | + final int? time; | ||
| 195 | + final int? value; | ||
| 196 | + final int? state; | ||
| 197 | + | ||
| 198 | + factory V2RealtimeStressItem.fromJson(Map<String, dynamic> json) { | ||
| 199 | + return V2RealtimeStressItem( | ||
| 200 | + time: _parseInt(json['time']), | ||
| 201 | + value: _parseInt(json['value']), | ||
| 202 | + state: _parseInt(json['state']), | ||
| 203 | + ); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + Map<String, dynamic> toJson() { | ||
| 207 | + final val = <String, dynamic>{}; | ||
| 208 | + if (time != null) val['time'] = time; | ||
| 209 | + if (value != null) val['value'] = value; | ||
| 210 | + if (state != null) val['state'] = state; | ||
| 211 | + return val; | ||
| 212 | + } | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +/// v2/realtime_stress/ response | ||
| 216 | +class V2RealtimeStressData { | ||
| 217 | + const V2RealtimeStressData({this.list}); | ||
| 218 | + | ||
| 219 | + final List<V2RealtimeStressItem>? list; | ||
| 220 | + | ||
| 221 | + factory V2RealtimeStressData.fromJson(Map<String, dynamic> json) { | ||
| 222 | + return V2RealtimeStressData( | ||
| 223 | + list: (json['list'] as List<dynamic>?) | ||
| 224 | + ?.map((e) => V2RealtimeStressItem.fromJson(e as Map<String, dynamic>)) | ||
| 225 | + .toList(), | ||
| 226 | + ); | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + Map<String, dynamic> toJson() { | ||
| 230 | + final val = <String, dynamic>{}; | ||
| 231 | + if (list != null) { | ||
| 232 | + val['list'] = list!.map((e) => e.toJson()).toList(); | ||
| 233 | + } | ||
| 234 | + return val; | ||
| 235 | + } | ||
| 236 | +} |
| @@ -22,17 +22,20 @@ class AppleLoginRequest { | @@ -22,17 +22,20 @@ class AppleLoginRequest { | ||
| 22 | const AppleLoginRequest({ | 22 | const AppleLoginRequest({ |
| 23 | this.loginType = 'apple', | 23 | this.loginType = 'apple', |
| 24 | required this.identityToken, | 24 | required this.identityToken, |
| 25 | + required this.userId, | ||
| 25 | this.email, | 26 | this.email, |
| 26 | this.nickname, | 27 | this.nickname, |
| 27 | }); | 28 | }); |
| 28 | 29 | ||
| 29 | final String loginType; | 30 | final String loginType; |
| 30 | final String identityToken; | 31 | final String identityToken; |
| 32 | + final String userId; | ||
| 31 | final String? email; | 33 | final String? email; |
| 32 | final String? nickname; | 34 | final String? nickname; |
| 33 | 35 | ||
| 34 | Map<String, dynamic> toJson() => { | 36 | Map<String, dynamic> toJson() => { |
| 35 | 'login_type': loginType, | 37 | 'login_type': loginType, |
| 38 | + 'apple_user_id': userId, | ||
| 36 | 'identity_token': identityToken, | 39 | 'identity_token': identityToken, |
| 37 | if (email != null) 'email': email, | 40 | if (email != null) 'email': email, |
| 38 | if (nickname != null) 'nickname': nickname, | 41 | if (nickname != null) 'nickname': nickname, |
-
Please register or login to post a comment