Commit a9c81df4cf84ca608cb4c2c991b378c8d57074b1

Authored by 刘宏哲
1 parent b6e5b8ac

feat(hm): add wechat alipay websocket

Showing 61 changed files with 2770 additions and 304 deletions
... ... @@ -34,6 +34,10 @@ class HealthKitHostApiImpl(
}
override fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit) {
callback(Result.success(false))
}
override fun fetchHrvData(
startTime: Long,
endTime: Long,
... ... @@ -88,7 +92,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchStandData(
... ... @@ -96,7 +100,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchStepCountData(
... ... @@ -104,7 +108,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchSleepData(
... ... @@ -112,7 +116,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthSleepUploadDataPoint>>) -> Unit
) {
}
override fun fetchSleepingWristTemperatureData(
... ... @@ -120,7 +124,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchRespiratoryRateData(
... ... @@ -128,7 +132,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchIrregularHeartRhythmData(
... ... @@ -136,7 +140,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<List<HealthUploadDataPoint>>) -> Unit
) {
}
override fun fetchActivityTargetData(
... ... @@ -144,7 +148,7 @@ class HealthKitHostApiImpl(
endTime: Long,
callback: (Result<HealthActivityTargetData?>) -> Unit
) {
}
}
... ...
... ... @@ -138,6 +138,7 @@ interface HealthKitHostApi {
/** Opens Huawei Health client authorization UI. Returns whether user granted. */
fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit)
companion object {
/** The codec used by HealthKitHostApi. */
... ... @@ -184,6 +185,24 @@ interface HealthKitHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.syncHealthDataToCloud{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(HealthKitApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
... ...
... ... @@ -184,6 +184,11 @@ protocol HealthKitHostApi {
/// Opens Huawei Health client authorization UI. Returns whether user granted.
func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void)
func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
/// does not wait for, or return, the subsequently available cloud data.
func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
... ... @@ -223,5 +228,24 @@ class HealthKitHostApiSetup {
} else {
requestHealthClientAuthorizationChannel.setMessageHandler(nil)
}
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
/// does not wait for, or return, the subsequently available cloud data.
let syncHealthDataToCloudChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
syncHealthDataToCloudChannel.setMessageHandler { _, reply in
api.syncHealthDataToCloud { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
syncHealthDataToCloudChannel.setMessageHandler(nil)
}
}
}
... ...
... ... @@ -51,4 +51,9 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
completion(.success(success))
}
}
func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void) {
// Huawei Health manual cloud synchronization is HarmonyOS-specific.
completion(.success(false))
}
}
... ...
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import '../../core/config/app_environment_config.dart';
... ... @@ -19,6 +21,7 @@ import '../../core/services/obs_upload_service.dart';
import '../../core/services/push_service.dart';
import '../../core/services/thinking_data_service.dart';
import '../../core/services/user_state_service.dart';
import '../../core/services/web_socket_service.dart';
import '../../data/local/local_storage.dart';
import '../../data/local/user_account_storage.dart';
import '../../data/local/user_preferences_storage.dart';
... ... @@ -42,6 +45,12 @@ void registerCoreDeps({
),
permanent: true,
);
final webSocketService = WebSocketService(
Get.find<AppEnvironmentConfig>(),
Get.find<UserPreferencesStorage>(),
);
WidgetsBinding.instance.addObserver(webSocketService);
Get.put(webSocketService, permanent: true);
}
/// User session, auth APIs, and login-side SDK wiring.
... ... @@ -53,6 +62,7 @@ void registerUserSessionDeps() {
Get.put(UserApi(dioClient), permanent: true);
Get.put(VipApi(dioClient), permanent: true);
Get.put(FriendApi(dioClient), permanent: true);
Get.put(HarmonyApi(dioClient), permanent: true);
Get.put<ImService>(ImServiceStub(), permanent: true);
Get.put(
ThinkingDataService(environmentConfig),
... ... @@ -76,6 +86,7 @@ void registerUserSessionDeps() {
Get.find<PushService>(),
Get.find<ThinkingDataService>(),
Get.find<HealthRawDataCoreService>(),
Get.find<WebSocketService>(),
),
permanent: true,
);
... ...
... ... @@ -40,6 +40,8 @@ abstract final class AppBootstrap {
localStorage: local,
).dependencies();
await userPreferencesStorage.syncLoginInfoToNative();
LoadingService.init();
if (local.termsAgreed) {
... ...
import 'dart:math' as math;
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/arc_progress_widget.dart';
import 'package:flutter/material.dart';
import '../models/activity_burn_report_models.dart';
class ActivityBurnRing extends StatefulWidget {
/// 活动消耗进度环。
///
/// 页面只依赖这一公共组件:OHOS 使用开口弧形样式,其它平台保留原有
/// 的三层闭合圆环,避免在各个报表页面重复处理平台分支。
class ActivityBurnRing extends StatelessWidget {
const ActivityBurnRing({
super.key,
required this.report,
... ... @@ -17,10 +23,84 @@ class ActivityBurnRing extends StatefulWidget {
final bool animate;
@override
State<ActivityBurnRing> createState() => _ActivityBurnRingState();
Widget build(BuildContext context) {
if (isOhos()) {
return _OhosActivityBurnRing(
report: report,
size: size,
animate: animate,
);
}
return _CircularActivityBurnRing(
report: report,
size: size,
animate: animate,
);
}
}
class _OhosActivityBurnRing extends StatelessWidget {
const _OhosActivityBurnRing({
required this.report,
required this.size,
required this.animate,
});
final ActivityBurnReport? report;
final double size;
final bool animate;
@override
Widget build(BuildContext context) {
final hasData = report?.hasData == true;
return ArcMultiProgressWidget(
size: size,
// Keep the visual proportion of the original 200px circular ring.
strokeWidth: size * 0.1,
gap: size * 0.02,
startAngleDeg: 140,
sweepTotalDeg: 260,
animate: animate,
items: [
ArcProgressItem(
ratio: hasData ? report?.activeEnergy?.rawProgress : null,
color: const Color(0xFFF84C38),
backgroundColor: const Color(0xFFFEE6E4),
),
ArcProgressItem(
ratio: hasData ? report?.exerciseMinutes?.rawProgress : null,
color: const Color(0xFFF6A523),
backgroundColor: const Color(0xFFFDF2E2),
),
ArcProgressItem(
ratio: hasData ? report?.standHours?.rawProgress : null,
color: const Color(0xFF3A9BFB),
backgroundColor: const Color(0xFFE3F1FD),
),
],
);
}
}
class _CircularActivityBurnRing extends StatefulWidget {
const _CircularActivityBurnRing({
required this.report,
required this.size,
required this.animate,
});
final ActivityBurnReport? report;
final double size;
final bool animate;
@override
State<_CircularActivityBurnRing> createState() =>
_CircularActivityBurnRingState();
}
class _ActivityBurnRingState extends State<ActivityBurnRing>
class _CircularActivityBurnRingState extends State<_CircularActivityBurnRing>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<_RingProgress> _animation;
... ... @@ -38,7 +118,7 @@ class _ActivityBurnRingState extends State<ActivityBurnRing>
}
@override
void didUpdateWidget(covariant ActivityBurnRing oldWidget) {
void didUpdateWidget(covariant _CircularActivityBurnRing oldWidget) {
super.didUpdateWidget(oldWidget);
final target = _target;
if (_RingProgress.fromReport(oldWidget.report) != target ||
... ... @@ -65,10 +145,8 @@ class _ActivityBurnRingState extends State<ActivityBurnRing>
if (visualDelta == 0) return Duration.zero;
// A tiny drawable change should not spend a full second creeping forward.
final milliseconds = (1000 * visualDelta.clamp(0, 1))
.round()
.clamp(150, 1000)
.toInt();
final milliseconds =
(1000 * visualDelta.clamp(0, 1)).round().clamp(150, 1000).toInt();
return Duration(milliseconds: milliseconds);
}
... ...
... ... @@ -107,6 +107,7 @@ class SubmitFeedbackController extends GetxController {
image.path,
_isVideo(image) ? HResourceType.video : HResourceType.image,
);
print("------, submit: $ossPath");
if (ossPath != null && ossPath.isNotEmpty) {
selectedPaths.add(ossPath);
}
... ...
... ... @@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:doublefeel_flutter/app/modules/friends/models/friend_stress_state.dart';
import 'package:doublefeel_flutter/app/modules/sleep_report/models/sleep_report_models.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
... ... @@ -278,7 +279,7 @@ class _FriendCardHeader extends StatelessWidget {
value: FriendCardAction.editRemark,
child: Center(child: Text(context.l10n.friendsEditRemark)),
),
if (!isOnWatchFace)
if (!isOhos() && !isOnWatchFace)
PopupMenuItem(
height: 42,
value: FriendCardAction.watchFace,
... ...
... ... @@ -7,6 +7,7 @@ import '../../../../core/config/app_environment.dart';
import '../../../../core/config/app_environment_config.dart';
import '../../../../core/constants/app_const.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/services/web_socket_service.dart';
import '../../../../core/util/app_toast.dart';
class DebugEnvironmentView extends StatelessWidget {
... ... @@ -69,6 +70,7 @@ class DebugEnvironmentView extends StatelessWidget {
await environmentConfig.setEnvironment(value);
dioClient.refreshBaseUrl();
Get.find<WebSocketService>().reconnect();
AppToast.show(
'环境已切换: ${environmentConfig.serverBaseUrl}',
);
... ...
... ... @@ -3,12 +3,9 @@ import 'dart:math' as math;
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/local/local_storage.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
... ...
... ... @@ -64,7 +64,10 @@ class MembershipOfferController extends GetxController {
if (result == AliPayResultCode.success) {
_trackSuccessPayOrder();
await _completeSuccessfulPurchase();
} else if (result == AliPayResultCode.cancel) {
AppToast.show(l10n.purchaseApplePaymentCancelled);
} else if (result == AliPayResultCode.error) {
AppToast.show(l10n.purchaseApplePaymentFailed);
if (isFromOnboard) {
Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
}
... ...
import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_offer_controller.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -37,6 +38,23 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
: l10n.freeRedemptionOffer,
enabled: true,
onPressed: () {
if (isOhos()) {
final product = controller.yearlyProduct.value;
if (product == null) {
controller.unlock();
return;
}
showPaymentConfirmationBottomSheet(
amount: _displayPriceOhos(product),
productName: _nonEmpty(product.name) ?? 'DoubleFeel年度会员',
paymentMethod: const PaymentMethod(
name: '支付宝支付',
iconAsset: 'assets/images/premium/ic_payment_alipay.png',
),
onPay: controller.unlock,
);
return;
}
controller.unlock();
},
),
... ...
... ... @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/core/constants/app_const.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/app_config_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
... ... @@ -16,7 +17,6 @@ import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -53,6 +53,7 @@ class PurchaseController extends GetxController {
final UserPreferencesStorage _userPreferences =
Get.find<UserPreferencesStorage>();
final AppPlatformHostApi _platformHostApi = AppPlatformHostApi();
final AlipayHostApi _alipayHostApi = AlipayHostApi();
final scrollController = ScrollController();
final planScrollController = ScrollController();
... ... @@ -135,6 +136,12 @@ class PurchaseController extends GetxController {
);
}
/// Whether the current purchase flow uses HarmonyOS payment methods.
bool get isOhos => _isOhos;
/// The plan currently selected on the purchase page.
PurchasePlan? get selectedPlan => _selectedPlan;
Future<void> restorePurchase() async {
if (isRestoring.value || isUnlocking.value) return;
... ... @@ -156,6 +163,15 @@ class PurchaseController extends GetxController {
_trackClickVipCenter('立即购买');
final plan = _selectedPlan;
final productId = plan?.product?.id;
if (_isOhos) {
if (productId == null) {
AppToast.show(l10n.purchaseProductInfoUnavailable);
return;
}
await _unlockWithAlipay(productId);
return;
}
final appleProductId = plan?.appleProductId;
if (productId == null || appleProductId == null || appleProductId.isEmpty) {
AppToast.show(l10n.purchaseProductInfoUnavailable);
... ... @@ -187,6 +203,41 @@ class PurchaseController extends GetxController {
}
}
/// Starts an App Pay transaction using the server-generated, RSA2-signed
/// order string. The Alipay App ID is part of that order string, so neither
/// the merchant private key nor any client-side signing is needed here.
Future<void> _unlockWithAlipay(int productId) async {
isUnlocking.value = true;
try {
final orderResult = await _payApi.createOrder(productId: productId);
if (orderResult case AppFailure<CreatePayOrderResponse>(:final error)) {
AppToast.show(error.displayMessage);
return;
}
if (orderResult is! AppSuccess<CreatePayOrderResponse>) return;
final prepayData = _nonEmpty(orderResult.data.prepayData);
if (prepayData == null) {
AppToast.show(l10n.purchaseOrderInfoUnavailable);
return;
}
final result = await _alipayHostApi.launchAliPay(prepayData);
if (result == AliPayResultCode.success) {
_trackSuccessPayOrder();
await _completeSuccessfulPurchase();
} else if (result == AliPayResultCode.cancel) {
AppToast.show(l10n.purchaseApplePaymentCancelled);
} else if (result == AliPayResultCode.error) {
AppToast.show(l10n.purchaseApplePaymentFailed);
}
} catch (_) {
AppToast.show(l10n.purchaseApplePaymentFailed);
} finally {
isUnlocking.value = false;
}
}
Future<void> loadProductList() async {
isLoadingProducts.value = true;
plans.clear();
... ... @@ -196,7 +247,7 @@ class PurchaseController extends GetxController {
if (result is! AppSuccess<PayProductListResponse>) return;
final products = (result.data.productList ?? const <PayProduct>[])
.where((product) => _nonEmpty(product.appleId) != null)
.where((product) => _isOhos || _nonEmpty(product.appleId) != null)
.toList();
plans.assignAll(products.map(_buildPlan));
final defaultProductIndex = products.indexWhere(
... ... @@ -205,11 +256,13 @@ class PurchaseController extends GetxController {
selectedIndex.value = defaultProductIndex >= 0 ? defaultProductIndex : 0;
isLoadingProducts.value = false;
await Future.wait(
products.indexed.map(
(entry) => _loadAppleProductInfo(entry.$1, entry.$2),
),
);
if (!_isOhos) {
await Future.wait(
products.indexed.map(
(entry) => _loadAppleProductInfo(entry.$1, entry.$2),
),
);
}
} finally {
isLoadingProducts.value = false;
}
... ... @@ -343,6 +396,24 @@ class PurchaseController extends GetxController {
}
PurchasePlan _buildPlan(PayProduct product, [AppleProductInfo? appleInfo]) {
if (_isOhos) {
// The product API may use 0 as the default value when no discount is
// configured. Treat only a positive discount price as an active offer;
// otherwise preserve the product's regular price.
final discountPrice = product.discountPrice;
final price = discountPrice != null && discountPrice > 0
? discountPrice
: product.price;
return PurchasePlan(
title: _nonEmpty(product.name) ?? '',
subtitle: _nonEmpty(product.content?.description) ?? '',
price: price == null ? '' : _formatAlipayPrice(price),
badge: _nonEmpty(product.content?.label) ?? '',
product: product,
actualPrice: price,
);
}
var productName = appleInfo?.productName ?? _nonEmpty(product.name) ?? '';
return PurchasePlan(
title: productName,
... ... @@ -382,6 +453,12 @@ class PurchaseController extends GetxController {
return appleInfo.originPrice.round();
}
String _formatAlipayPrice(int amountInCents) {
return ${(amountInCents / 100).toStringAsFixed(2)}';
}
bool get _isOhos => resolveAppPigeonPlatform() == AppPigeonPlatform.ohos;
String? _nonEmpty(String? value) {
final trimmed = value?.trim();
if (trimmed == null || trimmed.isEmpty) return null;
... ...
import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -85,7 +86,29 @@ class PurchaseView extends GetView<PurchaseController> {
() => PurchaseBottomBar(
label: context.l10n.purchaseUnlockNow,
loading: controller.isUnlocking.value,
onPressed: controller.unlock,
onPressed: () {
if (controller.isOhos) {
final plan = controller.selectedPlan;
if (plan == null) {
controller.unlock();
return;
}
showPaymentConfirmationBottomSheet(
amount: plan.price,
productName: plan.title.isEmpty
? 'DoubleFeel终身会员'
: plan.title,
paymentMethod: const PaymentMethod(
name: '支付宝支付',
iconAsset:
'assets/images/premium/ic_payment_alipay.png',
),
onPay: controller.unlock,
);
return;
}
controller.unlock();
},
onTermsTap: controller.openUserTerms,
onPrivacyTap: controller.openPrivacyPolicy,
),
... ...
import 'dart:math' as math;
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/arc_progress_widget.dart';
import 'package:flutter/material.dart';
import '../models/sleep_report_models.dart';
class SleepQualityRing extends StatefulWidget {
/// 睡眠报告进度环。
///
/// OHOS 使用开口弧形样式,其它平台维持原有的闭合双环;报表页面无需
/// 自行处理平台差异。
class SleepQualityRing extends StatelessWidget {
const SleepQualityRing({
super.key,
required this.report,
... ... @@ -13,10 +19,66 @@ class SleepQualityRing extends StatefulWidget {
final SleepReport? report;
@override
State<SleepQualityRing> createState() => _SleepQualityRingState();
Widget build(BuildContext context) => isOhos()
? _OhosSleepQualityRing(report: report)
: _CircularSleepQualityRing(report: report);
}
class _SleepQualityRingState extends State<SleepQualityRing>
class _OhosSleepQualityRing extends StatelessWidget {
const _OhosSleepQualityRing({required this.report});
final SleepReport? report;
@override
Widget build(BuildContext context) {
final durationSeconds =
report?.sleepDurationSeconds ?? (report?.duration?.minutes ?? 0) * 60;
final targetSeconds = report?.targetDurationSeconds;
final durationRatio =
durationSeconds <= 0 || targetSeconds == null || targetSeconds <= 0
? null
: (durationSeconds / targetSeconds).clamp(0, 1).toDouble();
final qualityScore = report?.validQualityScore;
return ArcMultiProgressWidget(
size: 200,
strokeWidth: 22,
gap: 4,
startAngleDeg: 140,
sweepTotalDeg: 260,
duration: const Duration(seconds: 1),
curve: Curves.easeInOutQuad,
items: [
ArcProgressItem(
ratio: durationRatio,
color: const Color(0xFF7B59EE),
backgroundColor: const Color(0xFFECE7FB),
noDataBackgroundColor: const Color(0xFFF0F1F5),
),
ArcProgressItem(
ratio: qualityScore == null
? null
: (qualityScore.clamp(0, 100) / 100).toDouble(),
color: const Color(0xFF638BFB),
backgroundColor: const Color(0xFFE8EFFD),
noDataBackgroundColor: const Color(0xFFF0F1F5),
),
],
);
}
}
class _CircularSleepQualityRing extends StatefulWidget {
const _CircularSleepQualityRing({required this.report});
final SleepReport? report;
@override
State<_CircularSleepQualityRing> createState() =>
_CircularSleepQualityRingState();
}
class _CircularSleepQualityRingState extends State<_CircularSleepQualityRing>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<double> _durationProgress;
... ... @@ -46,7 +108,7 @@ class _SleepQualityRingState extends State<SleepQualityRing>
}
@override
void didUpdateWidget(covariant SleepQualityRing oldWidget) {
void didUpdateWidget(covariant _CircularSleepQualityRing oldWidget) {
super.didUpdateWidget(oldWidget);
final oldQualityProgress =
((oldWidget.report?.validQualityScore ?? 0).clamp(0, 100) / 100)
... ...
import 'package:get/get.dart';
import '../controllers/hw_health_auth_webview_controller.dart';
class HwHealthAuthWebviewBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HwHealthAuthWebviewController>(
HwHealthAuthWebviewController.new,
);
}
}
... ...
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// OAuth callback payload returned after Huawei Health authorization finishes.
class HealthAuthEvent {
const HealthAuthEvent({this.code, this.state, this.error});
final String? code;
final String? state;
final String? error;
}
/// Arguments for the Huawei Health authorization WebView route.
class HwHealthAuthWebviewArguments {
const HwHealthAuthWebviewArguments({
required this.authorizationUrl,
this.onHealthAuthEvent,
});
final String authorizationUrl;
final void Function(HealthAuthEvent event)? onHealthAuthEvent;
}
/// GetX controller for the dedicated Huawei Health Kit OAuth WebView.
class HwHealthAuthWebviewController extends GetxController {
static const _callbackUrl =
'https://h5hosting.dbankcdn.com/cch5/healthkit/oauth-h5/oauth-callback.html';
late final WebViewController webViewController;
final isLoading = true.obs;
bool _callbackHandled = false;
HwHealthAuthWebviewArguments? get _arguments =>
Get.arguments is HwHealthAuthWebviewArguments
? Get.arguments as HwHealthAuthWebviewArguments
: null;
String get authorizationUrl => _arguments?.authorizationUrl ?? '';
@override
void onInit() {
super.onInit();
final target = Uri.tryParse(authorizationUrl);
webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: _handleNavigation,
onPageStarted: (_) => isLoading.value = true,
onPageFinished: (_) => isLoading.value = false,
onWebResourceError: (_) => isLoading.value = false,
),
)
..loadRequest(target ?? Uri.parse('about:blank'));
}
NavigationDecision _handleNavigation(NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri == null) return NavigationDecision.prevent;
final baseUrl = Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.hasPort ? uri.port : null,
path: uri.path,
).toString();
if (baseUrl != _callbackUrl) return NavigationDecision.navigate;
if (!_callbackHandled) {
_callbackHandled = true;
final event = HealthAuthEvent(
code: uri.queryParameters['code'],
state: uri.queryParameters['state'],
error: uri.queryParameters['error'],
);
_postHealthAuthEvent(event);
Get.back<HealthAuthEvent>(result: event);
}
return NavigationDecision.prevent;
}
void _postHealthAuthEvent(HealthAuthEvent event) {
_arguments?.onHealthAuthEvent?.call(event);
}
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../controllers/hw_health_auth_webview_controller.dart';
/// Dedicated WebView for the Huawei Health Kit OAuth authorization flow.
class HwHealthAuthWebviewPage extends GetView<HwHealthAuthWebviewController> {
const HwHealthAuthWebviewPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('')),
body: Stack(
children: [
WebViewWidget(controller: controller.webViewController),
Obx(
() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: const SizedBox.shrink(),
),
],
),
);
}
}
... ...
... ... @@ -3,10 +3,13 @@ import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.da
import 'package:doublefeel_flutter/app/modules/home/widgets/my/security_email_controller.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/bindings/membership_detail_binding.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/views/membership_detail_view.dart';
import 'package:doublefeel_flutter/app/modules/webview/bindings/hw_health_auth_webview_binding.dart';
import 'package:doublefeel_flutter/app/modules/webview/views/hw_health_auth_webview_page.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:get/get.dart';
import '../modules/account_settings/bindings/account_settings_binding.dart';
import '../modules/account_settings/views/account_settings_view.dart';
import '../modules/bind_partner/bindings/bind_partner_binding.dart';
import '../modules/bind_partner/views/bind_partner_view.dart';
import '../modules/config/bindings/developer_options_binding.dart';
... ... @@ -17,6 +20,8 @@ import '../modules/devtools/bindings/route_list_binding.dart';
import '../modules/devtools/views/route_list_view.dart';
import '../modules/feedback/feedback_list/bindings/feedback_list_binding.dart';
import '../modules/feedback/feedback_list/views/feedback_list_view.dart';
import '../modules/feedback/submit_feedback/bindings/submit_feedback_binding.dart';
import '../modules/feedback/submit_feedback/views/submit_feedback_view.dart';
import '../modules/friends/bindings/add_friend_binding.dart';
import '../modules/friends/bindings/friend_trend_binding.dart';
import '../modules/friends/views/add_friend_view.dart';
... ... @@ -25,10 +30,13 @@ import '../modules/help/bindings/help_binding.dart';
import '../modules/help/views/help_view.dart';
import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_page.dart';
import '../modules/home/widgets/my/security_email_views.dart';
import '../modules/login/bindings/login_binding.dart';
import '../modules/login/views/debug_environment_view.dart';
import '../modules/login/views/login_view.dart';
import '../modules/login/views/phone_login_view.dart';
import '../modules/membership_offer/bindings/membership_offer_binding.dart';
import '../modules/membership_offer/views/membership_offer_view.dart';
import '../modules/premium/bindings/premium_activated_binding.dart';
import '../modules/premium/views/premium_activated_view.dart';
import '../modules/privacy_settings/bindings/privacy_settings_binding.dart';
... ... @@ -37,20 +45,13 @@ import '../modules/purchase/bindings/purchase_binding.dart';
import '../modules/purchase/views/purchase_view.dart';
import '../modules/splash/bindings/splash_binding.dart';
import '../modules/splash/views/splash_page.dart';
import '../modules/feedback/submit_feedback/bindings/submit_feedback_binding.dart';
import '../modules/feedback/submit_feedback/views/submit_feedback_view.dart';
import '../modules/user_onboarding/bindings/user_onboarding_binding.dart';
import '../modules/user_onboarding/views/user_onboarding_view.dart';
import '../modules/membership_offer/bindings/membership_offer_binding.dart';
import '../modules/membership_offer/views/membership_offer_view.dart';
import '../modules/watch_theme/bindings/watch_theme_binding.dart';
import '../modules/watch_theme/views/custom_watch_theme_preview_view.dart';
import '../modules/watch_theme/views/create_watch_theme_view.dart';
import '../modules/watch_theme/views/custom_watch_theme_preview_view.dart';
import '../modules/watch_theme/views/watch_theme_preview_view.dart';
import '../modules/watch_theme/views/watch_theme_view.dart';
import '../modules/account_settings/bindings/account_settings_binding.dart';
import '../modules/account_settings/views/account_settings_view.dart';
import '../modules/home/widgets/my/security_email_views.dart';
import '../modules/webview/bindings/webview_binding.dart';
import '../modules/webview/views/webview_page.dart';
... ... @@ -108,6 +109,11 @@ abstract final class AppPages {
binding: WebviewBinding(),
),
GetPage(
name: Routes.HW_HEALTH_AUTH_WEB_VIEW,
page: () => const HwHealthAuthWebviewPage(),
binding: HwHealthAuthWebviewBinding(),
),
GetPage(
name: AppRoutes.userOnboarding,
page: () => const UserOnboardingView(),
binding: UserOnboardingBinding(),
... ...
... ... @@ -29,6 +29,7 @@ abstract class Routes {
static const ADD_SECURITY_EMAIL = _Paths.ADD_SECURITY_EMAIL;
static const CHANGE_SECURITY_EMAIL = _Paths.CHANGE_SECURITY_EMAIL;
static const RESET_PASSWORD = _Paths.RESET_PASSWORD;
static const HW_HEALTH_AUTH_WEB_VIEW = _Paths.HW_HEALTH_AUTH_WEB_VIEW;
}
abstract class _Paths {
... ... @@ -59,4 +60,5 @@ abstract class _Paths {
static const ADD_SECURITY_EMAIL = '/add-security-email';
static const CHANGE_SECURITY_EMAIL = '/change-security-email';
static const RESET_PASSWORD = '/reset-password';
static const HW_HEALTH_AUTH_WEB_VIEW = '/hw-health-auth-webview';
}
... ...
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
/// A payment method displayed in [PaymentConfirmationBottomSheet].
class PaymentMethod {
const PaymentMethod({
required this.name,
required this.iconAsset,
});
final String name;
final String iconAsset;
}
/// Opens the shared payment confirmation bottom sheet.
void showPaymentConfirmationBottomSheet({
required String amount,
required String productName,
required PaymentMethod paymentMethod,
required VoidCallback onPay,
}) {
Get.bottomSheet<void>(
PaymentConfirmationBottomSheet(
amount: amount,
productName: productName,
paymentMethod: paymentMethod,
onPay: onPay,
),
barrierColor: Colors.black.withValues(alpha: 0.72),
isScrollControlled: true,
enableDrag: false,
);
}
/// Shared confirmation sheet for a single, preselected payment method.
class PaymentConfirmationBottomSheet extends StatelessWidget {
const PaymentConfirmationBottomSheet({
super.key,
required this.amount,
required this.productName,
required this.paymentMethod,
required this.onPay,
});
final String amount;
final String productName;
final PaymentMethod paymentMethod;
final VoidCallback onPay;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF6F3FF),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 30,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text(
'待支付',
style: TextStyle(
color: Color(0xFF0F0F11),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Positioned(
left: 0,
child: IconButton(
onPressed: Get.back,
icon: const Icon(Icons.close_rounded),
color: const Color(0xFF8961F8),
iconSize: 22,
splashRadius: 20,
),
),
],
),
),
const SizedBox(height: 18),
Text(
amount,
style: const TextStyle(
color: Colors.black,
fontSize: 40,
fontWeight: FontWeight.w700,
height: 1,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 16,
height: 16,
clipBehavior: Clip.antiAlias,
padding: EdgeInsets.all(1),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
color: context.colors.primary),
child: Image.asset('assets/images/common/ic_pro.png'),
),
const SizedBox(width: 2),
Text(
productName,
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
),
),
],
),
const SizedBox(height: 22),
const Divider(height: 1, color: Color(0xFFE3DEEF)),
const Padding(
padding: EdgeInsets.only(top: 22, bottom: 14),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'支付方式',
style: TextStyle(
color: Color(0xFF0F0F11),
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
),
_PaymentMethodTile(paymentMethod: paymentMethod),
const SizedBox(height: 20),
SizedBox(
width: 280,
height: 48,
child: ElevatedButton(
onPressed: () {
Get.back();
onPay();
},
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: const Color(0xFF8961F8),
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: const Text('立即支付'),
),
),
],
),
),
),
);
}
}
class _PaymentMethodTile extends StatelessWidget {
const _PaymentMethodTile({required this.paymentMethod});
final PaymentMethod paymentMethod;
@override
Widget build(BuildContext context) {
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF845EEE)),
),
child: Row(
children: [
Image.asset(paymentMethod.iconAsset, width: 28, height: 28),
const SizedBox(width: 10),
Text(
paymentMethod.name,
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 15,
),
),
const Spacer(),
const Icon(Icons.check_circle_rounded,
color: Color(0xFF8961F8), size: 22),
],
),
);
}
}
... ...
... ... @@ -96,6 +96,23 @@ class AppEnvironmentConfig {
return resolveServerUrl(regionUrl);
}
/// WebSocket endpoint resolved from the active API host.
///
/// This keeps the socket in the same China/global and dev/xlab environment
/// as the REST APIs.
String get webSocketUrl {
final serverUri = Uri.parse(serverBaseUrl);
final webSocketScheme = serverUri.scheme == 'https' ? 'wss' : 'ws';
return serverUri
.replace(
scheme: webSocketScheme,
path: AppConst.webSocketPath,
query: null,
fragment: null,
)
.toString();
}
String resolveServerUrl(String url) {
return resolveServerUrlFor(environment.value, url);
}
... ...
... ... @@ -5,6 +5,9 @@ abstract final class AppConst {
static const String serverBaseUrl = 'https://api.doublefeel.cn';
static const String serverBaseUrlGlobal = 'https://api-oversea.doublefeel.cn';
/// WebSocket path supplied by the DoubleFeel server.
static const String webSocketPath = '/client/doublefeel/ws/';
static const String prefixXlabServerHost = 'xlab';
static const String prefixDevServerHost = 'dev';
... ...
import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/privacy_records.dart';
import '../../result/app_result.dart';
import '../../result/safe_call.dart';
import '../api_paths.dart';
import '../dio_client.dart';
class HarmonyApi {
HarmonyApi(this._dioClient);
final DioClient _dioClient;
Future<AppResult<void>> postHmAuth(String code) {
return safeCall(
call: () async {
await _dioClient.dio.post(
ApiPaths.hmAuth,
data: {
'code': code,
},
);
},
);
}
Future<AppResult<PrivacyRecordsResp>> getPrivacyRecords() {
return safeCall(
call: () async {
final response = await _dioClient.dio.get(ApiPaths.hmPrivacyRecords);
return PrivacyRecordsResp.fromJson(
response.data as Map<String, dynamic>,
);
},
errorHandlingPolicy:
HttpErrorHandlingPolicy(excludeErrorCodeList: {-100}),
);
}
/// startDate: 开始日期20260801
/// endDate: 截止日期20260801,和开始日期最大相隔31天
Future<AppResult<HmHealthData>> getHealthData(
HuaweiHealthDataType dataType, int startDate, int endDate) {
return safeCall(
call: () async {
final queryParameters = <String, dynamic>{
'date_type': dataType.dataType,
'start_date': startDate,
'end_date': endDate,
};
final response = await _dioClient.dio.get(
ApiPaths.hmHealthData,
queryParameters: queryParameters,
);
return HmHealthData.fromJson(
response.data as Map<String, dynamic>,
);
},
);
}
/// startDate: 开始日期20260801
/// endDate: 截止日期20260801,和开始日期最大相隔31天
Future<AppResult<HmSleepData>> getSleepData(int startDate, int endDate) {
return safeCall(
call: () async {
final queryParameters = <String, dynamic>{
'start_date': startDate,
'end_date': endDate,
};
final response = await _dioClient.dio.get(
ApiPaths.hmSleepData,
queryParameters: queryParameters,
);
return HmSleepData.fromJson(
response.data as Map<String, dynamic>,
);
},
);
}
}
... ...
... ... @@ -90,4 +90,11 @@ abstract final class ApiPaths {
static const friends = '/client/doublefeel/health/v2/friends/';
static const friendInfo = '/client/doublefeel/health/v2/friend_info/';
static const friendsSort = '/client/doublefeel/health/v2/friends/sort/';
// Harmony
static const hmAuth = '/client/doublefeel/huawei_hm/auth/';
static const hmHealthData = '/client/doublefeel/huawei_hm/health_data/';
static const hmSleepData = '/client/doublefeel/huawei_hm/sleep_data/';
static const hmPrivacyRecords = '/client/doublefeel/huawei_hm/privacy_records/';
}
... ...
import 'package:flutter/foundation.dart';
import 'package:doublefeel_flutter/pigeon/alipay_api.g.dart' as alipay;
import 'package:doublefeel_flutter/app/modules/webview/controllers/hw_health_auth_webview_controller.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/util/huawei_health_oauth.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart' as health_kit;
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'
as health_raw;
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart' as platform;
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart' as wear;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
export 'package:doublefeel_flutter/pigeon/alipay_api.g.dart';
export 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
... ... @@ -62,10 +67,7 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
Future<String> getFullUserAgent() => dispatch(
ios: () => _api.getFullUserAgent(),
ohos: () async {
//TODO: - 组装agent
return "";
},
ohos: () => _api.getFullUserAgent(),
);
Future<bool> isChinaRegion() => dispatch(
... ... @@ -76,50 +78,31 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
Future<int> getApnsAuthStatus() => dispatch(
ios: () => _api.getApnsAuthStatus(),
ohos: () async {
//TODO: - 检查通知权限
return 0;
},
ohos: () => _api.getApnsAuthStatus(),
);
Future<bool> requestNotificationAuth() => dispatch(
ios: () => _api.requestNotificationAuth(),
ohos: () async {
//TODO: - 请求通知权限
return false;
},
ohos: () => _api.requestNotificationAuth(),
);
Future<bool> isDebugEnvoriment() => dispatch(
ios: () => _api.isDebugEnvoriment(),
ohos: () async {
return false;
});
ohos: () => _api.isDebugEnvoriment());
Future<bool> shareText(String text) => dispatch(
ios: () => _api.shareText(text),
ohos: () async {
//TODO: - 分享文本(配对信息)
return false;
});
ios: () => _api.shareText(text), ohos: () => _api.shareText(text));
Future<bool> shareFileData(Uint8List databytes) => dispatch(
ios: () => _api.shareFileData(databytes),
ohos: () async {
return false;
});
ohos: () => _api.shareFileData(databytes));
Future<void> updateLoginInfo(String jsonString, String baseUrl) => dispatch(
ios: () => _api.updateLoginInfo(jsonString, baseUrl),
ohos: () async {
return;
});
ohos: () => _api.updateLoginInfo(jsonString, baseUrl));
Future<void> logout() => dispatch(
ios: () => _api.logout(),
ohos: () async {
return;
});
Future<void> logout() =>
dispatch(ios: () => _api.logout(), ohos: () => _api.logout());
Future<void> refreshVip() => dispatch(
ios: () => _api.refreshVip(),
... ... @@ -148,16 +131,12 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
Future<bool> nativeHandleUrl(String urlString) => dispatch(
ios: () => _api.nativeHandleUrl(urlString),
ohos: () async {
//TODO: - 处理url跳转
return false;
});
Future<String?> requestUnhandedUrl() => dispatch(
ios: () => _api.requestUnhandedUrl(),
ohos: () async {
//TODO: - 请求未处理的url(通知点击启动app后,启动flutter引擎交给flutter处理url)
return null;
});
ohos: () => _api.requestUnhandedUrl());
Future<platform.AppleSignInModel?> requestAppleSignIn() => dispatch(
ios: () => _api.requestAppleSignIn(),
... ... @@ -174,10 +153,7 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
Future<bool> sendEmail(String mailTo, String title, String content) =>
dispatch(
ios: () => _api.sendEmail(mailTo, title, content),
ohos: () async {
//TODO: - 发送邮件
return false;
});
ohos: () => _api.sendEmail(mailTo, title, content));
Future<platform.AppleProductInfo?> requestAppleProductInfo(
String productId,
... ... @@ -211,9 +187,8 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
) =>
dispatch(
ios: () => _api.uploadFile(filePath, resourceType),
ohos: () async {
//TODO: - 上传文件(头像)
return null;
ohos: () {
return _api.uploadFile(filePath, resourceType);
});
Future<String?> performCropImage(
... ... @@ -238,10 +213,8 @@ class AppPlatformHostApi extends _AppPigeonApiFacade {
dispatch(
ios: () => _api.sendLocalNotification(
dataType, dateTime, title, content, link),
ohos: () async {
//TODO: - 发送本地通知
return false;
});
ohos: () => _api.sendLocalNotification(
dataType, dateTime, title, content, link));
}
class AppHealthKitHostApi extends _AppPigeonApiFacade {
... ... @@ -257,16 +230,52 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade {
dispatch(
ios: () => _api.checkHealthAppAuthorization(),
ohos: () async {
//TODO: - 检查华为健康授权状态
return health_kit.HealthAuthorization(status: -1, hasData: false);
final harmonyApi = Get.find<HarmonyApi>();
var result = await harmonyApi.getPrivacyRecords();
var resp = 2;
switch (result) {
case AppSuccess(data: final info):
resp = info.opinion ?? 2;
break;
case AppFailure():
resp = 2;
break;
}
return health_kit.HealthAuthorization(status: resp, hasData: true);
});
Future<bool> requestHealthClientAuthorization() => dispatch(
ios: () => _api.requestHealthClientAuthorization(),
ohos: () async {
//TODO: - 请求华为健康授权
return false;
final state = HuaweiHealthOAuth.createState();
final result = await Get.toNamed(
Routes.HW_HEALTH_AUTH_WEB_VIEW,
arguments: HwHealthAuthWebviewArguments(
authorizationUrl:
HuaweiHealthOAuth.buildAuthorizationUrl(state: state),
),
);
final event = result is HealthAuthEvent ? result : null;
if (event == null || event.state != state) return false;
if (event.error != null || event.code == null) {
// 用户取消或华为授权失败
return false;
}
final code = event.code!;
final harmonyApi = Get.find<HarmonyApi>();
await harmonyApi.postHmAuth(code);
return true;
});
/// Requests Huawei Health to synchronize its local data to the cloud.
///
/// Available on HarmonyOS only. A `true` result means the request was
/// accepted; cloud data may become available shortly afterwards.
Future<bool> syncHealthDataToCloud() => dispatch(
ios: () async => false,
android: () async => false,
ohos: () => _api.syncHealthDataToCloud(),
);
}
class AppHealthKitRawDataHostApi extends _AppPigeonApiFacade {
... ...
import 'dart:async';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../constants/app_const.dart';
import '../logging/app_logger.dart';
import '../network/api/user_api.dart';
import '../util/platform.dart';
import '../platform/pigeon_api_facade.dart';
import '../result/app_result.dart';
/// HMS Push — stub until SDK is integrated.
class PushService {
/// Reports the HarmonyOS Push Kit token for the currently logged-in user.
class PushService with WidgetsBindingObserver {
PushService(this._userApi);
final UserApi _userApi;
static const _ohosPushChannel = MethodChannel('doublefeel_flutter/ohos_push');
static const _retryDelays = <Duration>[
Duration(seconds: 5),
Duration(seconds: 30),
Duration(minutes: 2),
];
Timer? _retryTimer;
bool _isRegistrationActive = false;
bool _needsRegistration = false;
bool _isUploading = false;
bool _isObservingLifecycle = false;
int _retryAttempt = 0;
Future<void> registerPushToken() async {
if (!isAndroid) {
if (isOhos()) {
return;
}
_isRegistrationActive = true;
_needsRegistration = true;
_retryAttempt = 0;
_retryTimer?.cancel();
_retryTimer = null;
_startObservingLifecycle();
await _reportPushToken();
}
/// Cancels retries when the authenticated session ends.
void cancelPushTokenRegistration() {
_isRegistrationActive = false;
_needsRegistration = false;
_retryAttempt = 0;
_retryTimer?.cancel();
_retryTimer = null;
_stopObservingLifecycle();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed &&
_needsRegistration &&
_retryTimer == null &&
_retryAttempt < _retryDelays.length) {
unawaited(_retryPendingPushToken());
}
}
Future<void> _retryPendingPushToken() async {
if (!_isRegistrationActive || !_needsRegistration || _isUploading) {
return;
}
await _reportPushToken();
}
Future<void> _reportPushToken() async {
if (!_isRegistrationActive || !_needsRegistration || _isUploading) {
return;
}
_isUploading = true;
var reported = false;
try {
// Always ask Push Kit again at login: tokens may be replaced after an
// app reinstall, device migration, or system upgrade.
final token = await _ohosPushChannel.invokeMethod<String>('getPushToken');
if (!_isRegistrationActive) {
return;
}
if (token == null || token.trim().isEmpty) {
AppLogger.w('HarmonyOS Push Kit returned an empty token');
return;
}
final deviceInfo = await AppPlatformHostApi().getFullUserAgent();
if (!_isRegistrationActive) {
return;
}
final result = await _userApi.postPushToken(
pushToken: token,
deviceInfo: deviceInfo,
pushPlatform: AppConst.pushPlatformHuawei,
);
if (result is AppFailure<void>) {
AppLogger.w('Failed to report HarmonyOS push token', result.error);
return;
}
AppLogger.i('HarmonyOS push token reported');
reported = true;
} on PlatformException catch (error) {
AppLogger.w('Failed to get HarmonyOS push token: ${error.code}', error);
} catch (error) {
AppLogger.w('Failed to register HarmonyOS push token', error);
} finally {
_isUploading = false;
if (reported) {
_needsRegistration = false;
_retryTimer?.cancel();
_retryTimer = null;
_stopObservingLifecycle();
} else if (_isRegistrationActive && _needsRegistration) {
_scheduleRetry();
}
}
}
void _scheduleRetry() {
if (!_needsRegistration || _retryTimer != null) {
return;
}
if (_retryAttempt >= _retryDelays.length) {
_stopObservingLifecycle();
return;
}
final delay = _retryDelays[_retryAttempt++];
_retryTimer = Timer(delay, () {
_retryTimer = null;
unawaited(_retryPendingPushToken());
});
}
void _startObservingLifecycle() {
if (_isObservingLifecycle) {
return;
}
WidgetsBinding.instance.addObserver(this);
_isObservingLifecycle = true;
}
void _stopObservingLifecycle() {
if (!_isObservingLifecycle) {
return;
}
// AppLogger.i('PushService.registerPushToken: HMS SDK pending');
// await _userApi.postPushToken(
// pushToken: 'pending-hms-token',
// deviceInfo: 'flutter',
// pushPlatform: AppConst.pushPlatformHuawei,
// );
WidgetsBinding.instance.removeObserver(this);
_isObservingLifecycle = false;
}
}
... ...
/// Data types accepted by the Huawei Health raw-data API.
///
/// Keep these numeric values aligned with Huawei's `data_type` contract.
enum HuaweiHealthDataType {
/// HRV(心率变异性)
hrv(1),
/// 心率
heartRate(2),
/// 血氧饱和度
bloodOxygenSaturation(3),
/// 活动量
activity(4),
/// 锻炼时长
exerciseDuration(5),
/// 站立时长
standingDuration(6),
/// 步数
stepCount(7),
/// 步行心率
walkingHeartRate(8),
/// 静息心率
restingHeartRate(9),
/// 睡眠心率
sleepingHeartRate(10),
/// 手腕温度
wristTemperature(11),
/// 睡眠呼吸频率
sleepingRespiratoryRate(12),
/// 房颤
atrialFibrillation(13);
const HuaweiHealthDataType(this.dataType);
final int dataType;
}
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
... ... @@ -8,6 +10,7 @@ import '../../data/local/user_preferences_storage.dart';
import 'im_service.dart';
import 'push_service.dart';
import 'thinking_data_service.dart';
import 'web_socket_service.dart';
/// Login lifecycle and SDK initialization.
class UserStateService {
... ... @@ -19,6 +22,7 @@ class UserStateService {
this._pushService,
this._thinkingDataService,
this._healthRawDataCoreService,
this._webSocketService,
);
final AppEnvironmentConfig _environmentConfig;
... ... @@ -28,6 +32,7 @@ class UserStateService {
final PushService _pushService;
final ThinkingDataService _thinkingDataService;
final HealthRawDataCoreService _healthRawDataCoreService;
final WebSocketService _webSocketService;
bool sdksInitialized = false;
... ... @@ -44,13 +49,18 @@ class UserStateService {
_trackLogin();
_setUserProfile();
_healthRawDataCoreService.openDatabase();
// Push Kit may wait for its service/network. The token is still reported
// after login, but must not delay the rest of the authenticated session.
unawaited(_pushService.registerPushToken());
await _imService.connect();
await _pushService.registerPushToken();
_webSocketService.start();
}
Future<void> onLogout({bool callServerLogout = true}) async {
_trackLogout();
_pushService.cancelPushTokenRegistration();
_imService.disconnect();
_webSocketService.stop();
_healthRawDataCoreService.closeDatabase();
if (callServerLogout) {
await _userApi.logout();
... ...
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/widgets.dart';
import '../../data/local/user_preferences_storage.dart';
import '../config/app_environment_config.dart';
import '../constants/network_const.dart';
import '../logging/app_logger.dart';
import '../network/user_agent_provider.dart';
/// Keeps the authenticated server WebSocket alive while the app is foregrounded.
///
/// Feature modules subscribe to their own server-defined [WebSocketEvent.xTag]
/// through [eventsFor]. The service owns transport only and contains no
/// feature-specific business behavior.
class WebSocketService with WidgetsBindingObserver {
WebSocketService(this._environmentConfig, this._userPreferencesStorage);
static const _connectTimeout = Duration(seconds: 15);
static const _initialReconnectDelay = Duration(seconds: 1);
static const _maxReconnectDelay = Duration(seconds: 60);
final AppEnvironmentConfig _environmentConfig;
final UserPreferencesStorage _userPreferencesStorage;
final StreamController<String> _messages = StreamController.broadcast();
final StreamController<WebSocketConnectionState> _states =
StreamController.broadcast();
final StreamController<WebSocketEvent> _events = StreamController.broadcast();
WebSocket? _socket;
StreamSubscription<dynamic>? _socketSubscription;
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
int _connectionGeneration = 0;
bool _shouldStayConnected = false;
bool _isConnecting = false;
bool _disposed = false;
/// Raw text messages sent by the server.
Stream<String> get messages => _messages.stream;
/// Connection changes, useful for diagnostics or a connection indicator.
Stream<WebSocketConnectionState> get states => _states.stream;
/// Parsed server events. Invalid JSON messages remain available from
/// [messages] but are not emitted here.
Stream<WebSocketEvent> get events => _events.stream;
/// Returns only events for a server-defined `x_tag`.
///
/// Each feature owns its tag and subscription lifecycle, keeping unrelated
/// WebSocket business logic out of this shared transport service.
Stream<WebSocketEvent> eventsFor(String xTag) =>
events.where((event) => event.xTag == xTag);
bool get isConnected => _socket != null && !_isConnecting;
/// Starts the connection when an authenticated session is available.
void start() {
if (_disposed) return;
_shouldStayConnected = true;
_connectIfNeeded();
}
/// Stops the connection permanently, until [start] is called again.
void stop() {
_shouldStayConnected = false;
_cancelReconnect();
_closeSocket();
_emitState(WebSocketConnectionState.disconnected);
}
/// Sends a text frame when the connection is open.
bool send(String message) {
final socket = _socket;
if (socket == null || _isConnecting) return false;
socket.add(message);
return true;
}
/// Reconnects immediately, for example after an environment change.
void reconnect() {
if (_disposed || !_shouldStayConnected) return;
_reconnectAttempts = 0;
_cancelReconnect();
_closeSocket();
_connectIfNeeded();
}
void _connectIfNeeded() {
if (_disposed ||
!_shouldStayConnected ||
_isConnecting ||
_socket != null ||
_reconnectTimer != null) {
return;
}
final accessToken = _userPreferencesStorage.accessToken;
if (accessToken.isEmpty) {
_emitState(WebSocketConnectionState.disconnected);
return;
}
_connect(accessToken);
}
Future<void> _connect(String accessToken) async {
_isConnecting = true;
final generation = ++_connectionGeneration;
_emitState(WebSocketConnectionState.connecting);
try {
var closeLateSocket = false;
final connectFuture = WebSocket.connect(
_environmentConfig.webSocketUrl,
headers: {
NetworkConst.headerAccessToken: accessToken,
NetworkConst.headerUserAgent: UserAgentProvider.userAgent,
NetworkConst.headerAcceptLanguage:
WidgetsBinding.instance.platformDispatcher.locale.languageCode,
},
);
unawaited(
connectFuture.then(
(socket) {
if (closeLateSocket) {
return socket.close(
WebSocketStatus.normalClosure,
'connection timed out',
);
}
},
onError: (_, __) {},
).catchError((_) {}),
);
final socket = await connectFuture.timeout(
_connectTimeout,
onTimeout: () {
closeLateSocket = true;
throw TimeoutException('WebSocket connection timed out');
},
);
if (!_isCurrentConnection(generation)) {
await socket.close();
return;
}
_socket = socket..pingInterval = const Duration(seconds: 30);
_reconnectAttempts = 0;
_isConnecting = false;
_emitState(WebSocketConnectionState.connected);
_socketSubscription = socket.listen(
_onMessage,
onError: (Object error, StackTrace stackTrace) {
_handleDisconnected(generation, error);
},
onDone: () => _handleDisconnected(generation, null),
cancelOnError: true,
);
} on WebSocketException catch (error, stackTrace) {
if (!_isCurrentConnection(generation)) return;
_isConnecting = false;
AppLogger.w('WebSocket connection failed', error, stackTrace);
_emitState(WebSocketConnectionState.disconnected);
if (_isAuthenticationHandshakeError(error.httpStatusCode)) {
AppLogger.w(
'WebSocket authentication failed; wait for a refreshed session',
error,
);
_shouldStayConnected = false;
_cancelReconnect();
return;
}
_scheduleReconnect();
} catch (error, stackTrace) {
if (!_isCurrentConnection(generation)) return;
_isConnecting = false;
AppLogger.w('WebSocket connection failed', error, stackTrace);
_emitState(WebSocketConnectionState.disconnected);
_scheduleReconnect();
}
}
bool _isAuthenticationHandshakeError(int? statusCode) =>
statusCode == HttpStatus.badRequest ||
statusCode == HttpStatus.unauthorized;
bool _isCurrentConnection(int generation) =>
!_disposed && _shouldStayConnected && generation == _connectionGeneration;
void _onMessage(dynamic data) {
if (data is! String) return;
if (!_messages.isClosed) _messages.add(data);
final event = WebSocketEvent.tryParse(data);
if (event != null && !_events.isClosed) _events.add(event);
}
void _handleDisconnected(int generation, Object? error) {
if (!_isCurrentConnection(generation)) return;
_socket = null;
_socketSubscription = null;
_isConnecting = false;
if (error != null) {
AppLogger.w('WebSocket disconnected with an error', error);
}
_emitState(WebSocketConnectionState.disconnected);
_scheduleReconnect();
}
void _scheduleReconnect() {
if (_disposed || !_shouldStayConnected || _reconnectTimer != null) return;
final exponent = _reconnectAttempts.clamp(0, 10);
final multiplier = 1 << exponent;
final delayMilliseconds =
(_initialReconnectDelay.inMilliseconds * multiplier).clamp(
_initialReconnectDelay.inMilliseconds,
_maxReconnectDelay.inMilliseconds,
);
_reconnectAttempts++;
_reconnectTimer = Timer(
Duration(milliseconds: delayMilliseconds),
() {
_reconnectTimer = null;
_connectIfNeeded();
},
);
}
void _cancelReconnect() {
_reconnectTimer?.cancel();
_reconnectTimer = null;
}
void _closeSocket() {
_connectionGeneration++;
final subscription = _socketSubscription;
_socketSubscription = null;
if (subscription != null) unawaited(subscription.cancel());
final socket = _socket;
_socket = null;
_isConnecting = false;
if (socket != null) {
unawaited(socket.close(WebSocketStatus.normalClosure, 'client closed'));
}
}
void _emitState(WebSocketConnectionState state) {
if (!_states.isClosed) _states.add(state);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
_connectIfNeeded();
return;
case AppLifecycleState.inactive:
return;
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
_cancelReconnect();
_closeSocket();
_emitState(WebSocketConnectionState.disconnected);
return;
}
}
void dispose() {
if (_disposed) return;
_disposed = true;
WidgetsBinding.instance.removeObserver(this);
_cancelReconnect();
_closeSocket();
unawaited(_messages.close());
unawaited(_states.close());
unawaited(_events.close());
}
}
enum WebSocketConnectionState { disconnected, connecting, connected }
/// Generic server WebSocket frame.
class WebSocketEvent {
const WebSocketEvent({
required this.messageId,
required this.messageType,
required this.toUserId,
required this.senderId,
required this.body,
required this.xTag,
});
final String? messageId;
final String? messageType;
final int? toUserId;
final int? senderId;
final Object? body;
final String? xTag;
static WebSocketEvent? tryParse(String message) {
try {
final decoded = jsonDecode(message);
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
return WebSocketEvent(
messageId: map['msg_id'] as String?,
messageType: map['msg_type'] as String?,
toUserId: (map['to_id'] as num?)?.toInt(),
senderId: (map['sender_id'] as num?)?.toInt(),
body: map['msg_body'],
xTag: map['x_tag'] as String?,
);
} catch (_) {
return null;
}
}
}
... ...
import 'dart:convert';
import 'dart:math';
/// Huawei Health Kit OAuth scopes required by DoubleFeel.
abstract final class HuaweiHealthScopes {
// 步数
// static const stepRead = 'https://www.huawei.com/healthkit/step.read';
// 活动小时数
// static const activeHoursRead = 'https://www.huawei.com/healthkit/activehours.read';
// 实时心率
// static const realtimeHeartRead = 'https://www.huawei.com/healthkit/extend/realtimeheart.read';
// 中高强度
// static const strengthRead = 'https://www.huawei.com/healthkit/strength.read';
//低血氧
// static const oxygenSaturationRead = 'https://www.huawei.com/healthkit/oxygensaturation.read';
// 日常活动数据
static const dailyActivitySummaryRead = "https://www.huawei.com/healthkit/dailyactivitysummary.read";
// Calories
static const caloriesRead = 'https://www.huawei.com/healthkit/calories.read';
// 运动目标
static const goalsRead = "https://www.huawei.com/healthkit/goals.read";
// 锻炼记录概要
static const activityRecordRead = "https://www.huawei.com/healthkit/activityrecord.read";
// 锻炼记录关联的原子采样明细数据权限
static const activityRead = "https://www.huawei.com/healthkit/activity.read";
// 心率
static const heartRateRead = 'https://www.huawei.com/healthkit/heartrate.read';
// 压力
static const stressRead = 'https://www.huawei.com/healthkit/stress.read';
// 睡眠
static const sleepRead = 'https://www.huawei.com/healthkit/sleep.read';
// 年数据
static const historyDataOpenYear = 'https://www.huawei.com/healthkit/historydata.open.year';
// 手动同步数据
static const cloudsync = 'https://www.huawei.com/healthkit/huaweihealthdata.cloudsync';
static const requested = <String>[
dailyActivitySummaryRead,
caloriesRead,
goalsRead,
activityRecordRead,
activityRead,
heartRateRead,
stressRead,
sleepRead,
historyDataOpenYear,
cloudsync,
'openid',
'profile',
];
}
/// Builds Huawei Health Kit OAuth authorization URLs in the Flutter layer.
abstract final class HuaweiHealthOAuth {
static const _clientId = '6917602291754541850';
static const _redirectUri =
'https://h5hosting.dbankcdn.com/cch5/healthkit/oauth-h5/oauth-callback.html';
static final Random _random = Random.secure();
/// Generates an OAuth state value. Persist and validate it after redirect.
static String createState() {
final bytes = List<int>.generate(32, (_) => _random.nextInt(256));
return base64UrlEncode(bytes).replaceAll('=', '');
}
/// Returns the Huawei authorization URL for the configured Health Kit scopes.
///
/// Pass a persisted [state] so the callback can be verified against it.
static String buildAuthorizationUrl({String? state}) {
return Uri.https(
'oauth-login.cloud.huawei.com',
'/oauth2/v3/authorize',
<String, String>{
'client_id': _clientId,
'response_type': 'code',
'redirect_uri': _redirectUri,
'scope': HuaweiHealthScopes.requested.join(' '),
'state': state ?? createState(),
'display': 'touch',
'access_type': 'offline',
},
).toString();
}
}
... ...
import 'dart:convert';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
... ... @@ -45,9 +45,11 @@ class UserPreferencesStorage {
} catch (_) {
preferences.value = UserPreferences.empty;
}
_syncLoginInfoToNative(userInfo: preferences.value);
}
Future<void> syncLoginInfoToNative() =>
_syncLoginInfoToNative(userInfo: preferences.value);
Future<void> _syncLoginInfoToNative(
{required UserPreferences userInfo}) async {
if (userInfo.accessToken.isEmpty) {
... ...
class HmHealthData {
HmHealthData({
this.list,
});
HmHealthData.fromJson(dynamic json) {
if (json['list'] != null) {
list = [];
json['list'].forEach((v) {
list?.add(HmHealthDataItem.fromJson(v));
});
}
}
List<HmHealthDataItem>? list;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (list != null) {
map['list'] = list?.map((v) => v.toJson()).toList();
}
return map;
}
}
class HmHealthDataItem {
HmHealthDataItem({
this.time,
this.dataType,
this.value,
this.isAsleep,
});
HmHealthDataItem.fromJson(dynamic json) {
time = json['time'];
dataType = json['data_type'];
value = json['value'];
isAsleep = json['is_asleep'];
}
num? time;
num? dataType;
num? value;
num? isAsleep;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['time'] = time;
map['data_type'] = dataType;
map['value'] = value;
map['is_asleep'] = isAsleep;
return map;
}
}
... ...
class HmSleepData {
HmSleepData({
this.list,
});
HmSleepData.fromJson(dynamic json) {
if (json['list'] != null) {
list = [];
json['list'].forEach((v) {
list?.add(HmSleepDataItem.fromJson(v));
});
}
}
List<HmSleepDataItem>? list;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (list != null) {
map['list'] = list?.map((v) => v.toJson()).toList();
}
return map;
}
}
class HmSleepDataItem {
HmSleepDataItem({
this.fromTime,
this.toTime,
this.dataType,
});
HmSleepDataItem.fromJson(dynamic json) {
fromTime = json['from_time'];
toTime = json['to_time'];
dataType = json['data_type'];
}
num? fromTime;
num? toTime;
num? dataType;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['from_time'] = fromTime;
map['to_time'] = toTime;
map['data_type'] = dataType;
return map;
}
}
... ...
// ─── Responses ───────────────────────────────────────────────────────────────
class PrivacyRecordsResp {
const PrivacyRecordsResp({this.opinion});
final int? opinion;
factory PrivacyRecordsResp.fromJson(Map<String, dynamic> json) {
return PrivacyRecordsResp(opinion: json['opinion'] as int?);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (opinion != null) val['opinion'] = opinion;
return val;
}
}
... ...
... ... @@ -173,4 +173,36 @@ class HealthKitHostApi {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
/// does not wait for, or return, the subsequently available cloud data.
Future<bool> syncHealthDataToCloud() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
}
... ...
... ... @@ -3,7 +3,7 @@
"bundleName": "com.doublefeel.hmapp",
"vendor": "example",
"versionCode": 1000000,
"versionName": "2.4.5",
"versionName": "2.7.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
... ...
... ... @@ -2,7 +2,7 @@
"string": [
{
"name": "app_name",
"value": "doublefeel_flutter"
"value": "DoubleFeel"
}
]
}
... ...

6.63 KB | W: | H:

3.86 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
========================
QA
========================
# 构建 QA 包(给测试人员使用)
hvigorw --mode module -p module=entry@qa -p product=qa -p buildMode=release assembleHap
========================
Release
========================
# 构建 Release 包
hvigorw --mode module -p module=entry@release -p product=release -p buildMode=release assembleHap
\ No newline at end of file
... ...
... ... @@ -47,7 +47,51 @@
"signingConfig": "auto",
"targetSdkVersion": "6.0.0(20)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS"
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
},
"arkOptions": {
"buildProfileFields": {
"enableEnvironmentSwitch": false
}
}
}
},
{
"name": "qa",
"signingConfig": "release",
"targetSdkVersion": "6.0.0(20)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
},
"arkOptions": {
"buildProfileFields": {
"enableEnvironmentSwitch": true
}
}
}
},
{
"name": "release",
"signingConfig": "release",
"targetSdkVersion": "6.0.0(20)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
},
"arkOptions": {
"buildProfileFields": {
"enableEnvironmentSwitch": false
}
}
}
}
],
"buildModeSet": [
... ... @@ -70,7 +114,9 @@
{
"name": "default",
"applyToProducts": [
"default"
"default",
"qa",
"release"
]
}
]
... ...
{
"name": "entry",
"version": "1.0.0",
... ... @@ -6,6 +5,9 @@
"main": "",
"author": "",
"license": "",
"dependencies": {},
"dependencies": {
"@obs/esdk-obs-harmony": "3.24.9"
},
"devDependencies": {},
"dynamicDependencies": {}
}
... ...
... ... @@ -6,8 +6,17 @@ import { WeChatLoginBridge } from '../native/WeChatLoginBridge';
import { WeChatHostApi } from '../pigeon/WeChatApi';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import Any from '@ohos/flutter_ohos/src/main/ets/plugin/common/Any';
import MethodCall from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall';
import MethodChannel, { MethodCallHandler, MethodResult } from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel';
import { BusinessError } from '@kit.BasicServicesKit';
import { pushService } from '@kit.PushKit';
export default class EntryAbility extends FlutterAbility {
export default class EntryAbility extends FlutterAbility implements MethodCallHandler {
private static readonly FLUTTER_MAIN_CHANNEL = 'doublefeel_flutter_main_channel';
private static readonly OHOS_PUSH_CHANNEL = 'doublefeel_flutter/ohos_push';
private flutterUrlChannel?: MethodChannel;
private pushTokenChannel?: MethodChannel;
private weChatLoginBridge?: WeChatLoginBridge;
configureFlutterEngine(flutterEngine: FlutterEngine) {
... ... @@ -17,6 +26,17 @@ export default class EntryAbility extends FlutterAbility {
flutterEngine.getDartExecutor()!.getBinaryMessenger()!,
this.context,
)
this.flutterUrlChannel = new MethodChannel(
flutterEngine.getDartExecutor()!.getBinaryMessenger()!,
EntryAbility.FLUTTER_MAIN_CHANNEL,
)
this.pushTokenChannel = new MethodChannel(
flutterEngine.getDartExecutor()!.getBinaryMessenger()!,
EntryAbility.OHOS_PUSH_CHANNEL,
)
this.pushTokenChannel.setMethodCallHandler(this)
NativePigeonRegistrar.setAppForeground(true)
NativePigeonRegistrar.handleWant(this.getWant())
this.weChatLoginBridge = new WeChatLoginBridge(this.context)
WeChatHostApi.setup(
flutterEngine.getDartExecutor()!.getBinaryMessenger()!,
... ... @@ -25,8 +45,58 @@ export default class EntryAbility extends FlutterAbility {
this.weChatLoginBridge.handleWant(this.getWant())
}
onMethodCall(call: MethodCall, result: MethodResult): void {
if (call.method !== 'getPushToken') {
result.notImplemented()
return
}
// Push Kit obtains a token asynchronously. Do not request notification
// permission here: token issuance and the user's notification preference
// are independent, and permission is requested by the product flow.
pushService.getToken()
.then((token: string) => result.success(token))
.catch((error: BusinessError) => {
result.error('push_token_unavailable', error.message, error.code)
})
}
onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void {
super.onNewWant(want, launchParams)
NativePigeonRegistrar.handleWant(want)
this.forwardFlutterUrl(want)
this.weChatLoginBridge?.handleWant(want)
}
/**
* Mirrors iOS notification/deep-link handling for an existing Flutter
* engine. Keep the URL cached until Flutter confirms it handled the route,
* so a not-yet-ready engine can still consume it during startup.
*/
private forwardFlutterUrl(want: Want): void {
const url = want.uri;
if (url === undefined || url.length === 0) {
return;
}
const result: MethodResult = {
success: (handled: Any): void => {
if (handled === true) {
NativePigeonRegistrar.clearUnhandedUrl(url)
}
},
error: (_code: string, _message: string, _details: Any): void => {},
notImplemented: (): void => {},
}
this.flutterUrlChannel?.invokeMethod('handleFlutterUrl', { url }, result)
}
onForeground(): void {
super.onForeground()
NativePigeonRegistrar.setAppForeground(true)
}
onBackground(): void {
super.onBackground()
NativePigeonRegistrar.setAppForeground(false)
}
}
... ...
import { AlipayHostApi, AliPayResultCode } from '../pigeon/AlipayApi';
import {
AlipayHostApi, AliPayResultCode, FlutterError, Result as PigeonResult,
} from '../pigeon/AlipayApi';
import { Pay } from '@cashier_alipay/cashiersdk';
const ALIPAY_APP_ID = '2021006185683254';
/**
* Alipay host implementation for HarmonyOS.
* Alipay SDK bridge for HarmonyOS.
*
* Currently a stub – replace with the real Alipay HarmonyOS SDK call
* once the SDK is available.
* `prepayData` is an order string produced and signed by the server. Do not
* construct or sign it in the app: the order string contains the Alipay App ID
* (2021006185683254) and must be signed with the merchant private key.
*/
export class AlipayHostApiImpl extends AlipayHostApi {
launchAliPay(_prepayData: string): AliPayResultCode {
return AliPayResultCode.UNSUPPORTED;
launchAliPay(
prepayData: string,
callback: PigeonResult<AliPayResultCode>,
): void {
console.log("------", "launchAliPay prepayData: " + prepayData)
new Pay().pay(prepayData, true).then((result) => {
switch (result.get('resultStatus')) {
case '9000':
callback.success(AliPayResultCode.SUCCESS);
return;
case '6001':
callback.success(AliPayResultCode.CANCEL);
return;
default:
callback.success(AliPayResultCode.ERROR);
return;
}
}).catch(() => callback.success(AliPayResultCode.ERROR));
}
}
... ...
... ... @@ -77,18 +77,20 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
});
}
override cancelHealthAppAuthorization(): boolean {
return false;
/**
* Triggers the manual device-to-cloud synchronization exposed by Huawei
* Health. The application must have obtained the "manual data sync"
* permission in the Health Service Kit console before this call can work.
*/
syncHealthDataToCloud(result: Result<boolean>): void {
this.syncAllHealthData().then(() => {
result.success(true);
}).catch((error: Error) => {
console.error(`syncHealthDataToCloud failed: ${error.name}: ${error.message}`);
result.success(false);
});
}
// cancelHealthAppAuthorization(result: Result<boolean>): void {
// this.ensureInitialized()
// .then(() => healthStore.cancelAuthorizations())
// .then(() => result.success(true))
// .catch(() => result.success(false));
// }
private async getAuthorization(): Promise<healthStore.AuthorizationResponse> {
await this.ensureInitialized();
return healthStore.getAuthorizations(this.authorizationRequest);
... ... @@ -99,6 +101,11 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
return healthStore.requestAuthorizations(this.context, this.authorizationRequest);
}
private async syncAllHealthData(): Promise<void> {
await this.ensureInitialized();
await healthStore.syncAll();
}
private async ensureInitialized(): Promise<void> {
await healthStore.init(this.context);
}
... ...
... ... @@ -5,13 +5,32 @@ import { BinaryMessenger } from '@ohos/flutter_ohos/src/main/ets/plugin/common/B
import { AlipayHostApiImpl } from './AlipayHostApiImpl';
import { HealthKitHostApiImpl } from './HealthKitHostApiImpl';
import { WearEngineHostApiImpl } from './WearEngineHostApiImpl';
import { PlatformHostApi } from '../pigeon/PlatformApi';
import { PlatformHostApiImpl } from './PlatformHostApiImpl';
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
/** Registers all Pigeon host-API implementations with the Flutter binary messenger. */
export class NativePigeonRegistrar {
private static platformHostApi?: PlatformHostApiImpl;
static register(binaryMessenger: BinaryMessenger, context: common.UIAbilityContext): void {
HealthKitHostApi.setup(binaryMessenger, new HealthKitHostApiImpl(context));
WearEngineHostApi.setup(binaryMessenger, new WearEngineHostApiImpl());
AlipayHostApi.setup(binaryMessenger, new AlipayHostApiImpl());
NativePigeonRegistrar.platformHostApi = new PlatformHostApiImpl(context);
PlatformHostApi.setup(binaryMessenger, NativePigeonRegistrar.platformHostApi);
}
static handleWant(want: Want): void {
NativePigeonRegistrar.platformHostApi?.handleWant(want);
}
static clearUnhandedUrl(url: string): void {
NativePigeonRegistrar.platformHostApi?.clearUnhandedUrl(url);
}
static setAppForeground(isForeground: boolean): void {
NativePigeonRegistrar.platformHostApi?.setAppForeground(isForeground);
}
}
... ...
import ObsClient from '@obs/esdk-obs-harmony';
import { http } from '@kit.NetworkKit';
import { notificationManager } from '@kit.NotificationKit';
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { bundleManager, wantAgent } from '@kit.AbilityKit';
import { productViewManager } from '@kit.AppGalleryKit';
import { BusinessError, request } from '@kit.BasicServicesKit';
import preferences from '@ohos.data.preferences';
import webview from '@ohos.web.webview';
import deviceInfo from '@ohos.deviceInfo';
import display from '@ohos.display';
import fileIo from '@ohos.file.fs';
import fileUri from '@ohos.file.fileuri';
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
import {
AppleProductInfo,
AppleProductPaymentResult,
AppleSignInModel,
FlutterError,
GoogleSignInModel,
HResourceType,
PlatformHostApi,
Result,
} from '../pigeon/PlatformApi';
import BuildProfile from 'BuildProfile';
interface SessionSnapshot {
access_token?: string;
}
interface TempCredential {
access_key?: string;
secret_key?: string;
security_token?: string;
}
interface TempTokenResponse {
token?: TempCredential;
keys?: string[];
host?: string;
endpoint?: string;
bucket?: string;
}
const NOTIFICATION_PREFERENCES_NAME = 'platform_host_api';
const NOTIFICATION_AUTH_REQUESTED_KEY = 'notification_auth_requested';
/**
* HarmonyOS implementation of the shared PlatformHostApi.
*
* Its OBS flow deliberately matches iOS: request a scoped, short-lived
* credential from the API, then upload directly to OBS with the native SDK.
*/
export class PlatformHostApiImpl extends PlatformHostApi {
private readonly context: common.UIAbilityContext;
private baseUrl: string = '';
private accessToken: string = '';
private unhandedUrl: string | undefined;
private isAppForeground: boolean = true;
constructor(context: common.UIAbilityContext) {
super();
this.context = context;
}
getFullUserAgent(): string {
let versionCode = '';
let versionName = '';
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION,
);
versionCode = bundleInfo.versionCode.toString();
versionName = bundleInfo.versionName;
} catch (_error) {
}
let hardware = '';
let osVersion = '';
try {
hardware = deviceInfo.hardwareModel || deviceInfo.productModel;
osVersion = deviceInfo.osFullName;
} catch (_error) {
}
let height = '';
let width = '';
try {
const screen = display.getDefaultDisplaySync();
height = screen.height.toString();
width = screen.width.toString();
} catch (_error) {
}
// Do not leave a leading space when HarmonyOS cannot provide the WebView
// UA. A leading space can be rendered as a folded HTTP header line.
// Strip line breaks as well, since a User-Agent must be a single header
// value.
const webUserAgent = this.getDefaultWebUserAgent()
.replace(/[\r\n]+/g, ' ')
.trim();
const appUserAgent = `doublefeel/${versionCode}(${versionName})` +
`(${hardware}; HarmonyOS ${osVersion}; ${height}x${width})`;
return webUserAgent.length > 0
? `${webUserAgent} ${appUserAgent}`
: appUserAgent;
}
private getDefaultWebUserAgent(): string {
try {
return webview.WebviewController.getDefaultUserAgent();
} catch (_error) {
return '';
}
}
isChinaRegion(result: Result<boolean>): void {
result.success(true);
}
getApnsAuthStatus(result: Result<number>): void {
this.getNotificationAuthorizationStatus()
.then((status: number) => result.success(status))
.catch((error: Error) => result.error(this.toPlatformFlutterError(error, 'notification_status_failed')));
}
requestNotificationAuth(result: Result<boolean>): void {
notificationManager.isNotificationEnabled()
.then((enabled: boolean) => {
if (enabled) {
result.success(true);
return;
}
return notificationManager.requestEnableNotification(this.context)
.then(() => this.markNotificationAuthorizationRequested())
.then(() => notificationManager.isNotificationEnabled())
.then((granted: boolean) => result.success(granted));
})
.catch((error: Error) => {
// HarmonyOS reports 1600004 when the notification dialog cannot be
// shown again after the user has declined it. This is a normal denied
// result, not a platform-call failure. Take the user to the app
// settings immediately because the dialog cannot be reopened.
if ((error as BusinessError).code === 1600004) {
return this.markNotificationAuthorizationRequested()
.then(() => this.openNotificationSettings())
.then(() => result.success(false))
.catch((storeError: Error) =>
result.error(this.toPlatformFlutterError(storeError, 'notification_permission_failed')),
);
}
return result.error(this.toPlatformFlutterError(error, 'notification_permission_failed'));
});
}
isDebugEnvoriment(): boolean {
return BuildProfile.DEBUG || BuildProfile.enableEnvironmentSwitch;
}
shareText(text: string, result: Result<boolean>): void {
const data = new systemShare.SharedData({
utd: utd.UniformDataType.TEXT,
content: text,
title: text,
});
this.showSharedData(data, result);
}
shareFileData(databytes: number[], result: Result<boolean>): void {
if (databytes.length === 0) {
result.success(false);
return;
}
const filePath =
`${this.context.cacheDir}/doublefeel-share-${Date.now()}-${Math.floor(Math.random() * 1000000)}.data`;
try {
const file = fileIo.openSync(
filePath,
fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC,
);
try {
fileIo.writeSync(file.fd, new Uint8Array(databytes).buffer);
} finally {
fileIo.closeSync(file);
}
const data = new systemShare.SharedData({
utd: utd.UniformDataType.FILE,
uri: fileUri.getUriFromPath(filePath),
title: 'doublefeel-data.data',
});
this.showSharedData(data, result, () => this.deleteSharedFile(filePath));
} catch (error) {
this.deleteSharedFile(filePath);
result.error(this.toPlatformFlutterError(error, 'share_file_failed'));
}
}
private showSharedData(
data: systemShare.SharedData,
result: Result<boolean>,
onFinished?: () => void,
): void {
const controller = new systemShare.ShareController(data);
let settled = false;
const onShareCompleted = (_shareResult: systemShare.ShareOperationResult): void => {
finish(true);
};
const onDismiss = (): void => {
finish(false);
};
const finish = (shared: boolean): void => {
if (settled) {
return;
}
settled = true;
controller.off('shareCompleted', onShareCompleted);
controller.off('dismiss', onDismiss);
onFinished?.();
result.success(shared);
};
controller.on('shareCompleted', onShareCompleted);
controller.on('dismiss', onDismiss);
controller.show(this.context, {})
.catch((error: Error) => {
if (settled) {
return;
}
settled = true;
controller.off('shareCompleted', onShareCompleted);
controller.off('dismiss', onDismiss);
onFinished?.();
result.error(this.toPlatformFlutterError(error, 'share_failed'));
});
}
private deleteSharedFile(filePath: string): void {
try {
fileIo.unlinkSync(filePath);
} catch (_error) {
}
}
updateLoginInfo(jsonString: string, baseUrl: string): void {
try {
const session = JSON.parse(jsonString) as SessionSnapshot;
this.accessToken = session.access_token ?? '';
this.baseUrl = baseUrl;
} catch (_error) {
this.accessToken = '';
this.baseUrl = '';
}
}
logout(): void {
this.accessToken = '';
this.baseUrl = '';
}
refreshVip(): void {
}
refreshWatchAppAndWidgets(): void {
}
requestAppReview(result: Result<boolean>): void {
// AppGallery Kit opens this app's product page, where users can submit a
// review. This is the HarmonyOS equivalent of opening the App Store review
// page on iOS.
const want: Want = {
parameters: {
bundleName: this.context.abilityInfo.bundleName,
},
};
try {
productViewManager.loadProduct(this.context, want);
result.success(true);
} catch (error) {
result.error(this.toPlatformFlutterError(error as Error, 'request_app_review_failed'));
}
}
jumpAppSetting(): boolean {
return false;
}
nativeHandleUrl(_urlString: string): boolean {
return false;
}
requestUnhandedUrl(): string | undefined {
const url = this.unhandedUrl;
this.unhandedUrl = undefined;
return url;
}
clearUnhandedUrl(url: string): void {
if (this.unhandedUrl === url) {
this.unhandedUrl = undefined;
}
}
handleWant(want: Want): void {
if (want.uri !== undefined && want.uri.length > 0) {
this.unhandedUrl = want.uri;
}
}
setAppForeground(isForeground: boolean): void {
this.isAppForeground = isForeground;
}
requestAppleSignIn(result: Result<AppleSignInModel | undefined>): void {
result.success(undefined);
}
requestGoogleSignIn(result: Result<GoogleSignInModel | undefined>): void {
result.success(undefined);
}
sendEmail(mailTo: string, title: string, content: string, result: Result<boolean>): void {
if (mailTo.length === 0) {
result.success(false);
return;
}
const want: Want = {
action: 'ohos.want.action.sendToData',
uri: `mailto:${encodeURIComponent(mailTo)}?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(
content)}`,
type: 'message/rfc822',
};
this.context.startAbility(want)
.then(() => result.success(true))
.catch((error: Error) => result.error(this.toPlatformFlutterError(error, 'send_email_failed')));
}
requestAppleProductInfo(
_productId: string,
_baseUnit: number,
result: Result<AppleProductInfo | undefined>,
): void {
result.success(undefined);
}
performApplePayment(
_productId: string,
_uuid: string,
result: Result<AppleProductPaymentResult | undefined>,
): void {
result.success(undefined);
}
performRestore(result: Result<boolean>): void {
result.success(false);
}
uploadFile(filePath: string, resourceType: HResourceType, result: Result<string | undefined>): void {
this.upload(filePath, resourceType)
.then((fileUrl: string) => {
console.log("------, uploadFile success: " + fileUrl)
return result.success(fileUrl)
})
.catch((error: Error) => {
console.log("------, uploadFile error: " + error)
return result.error(this.toFlutterError(error))
});
}
performCropImage(
_imageUrl: string,
_maxKB: number | undefined,
_width: number | undefined,
_height: number | undefined,
result: Result<string | undefined>,
): void {
result.success(undefined);
}
sendLocalNotification(
dataType: number,
dateTime: number,
title: string,
content: string,
link: string,
result: Result<boolean>,
): void {
if (this.isAppForeground) {
result.success(false);
return;
}
const notificationId = Math.abs(Math.trunc(dataType * 1000000000 + dateTime)) % 2147483647;
notificationManager.isNotificationEnabled()
.then((enabled: boolean) => {
if (!enabled) {
result.success(false);
return;
}
return this.publishLocalNotification(notificationId, title, content, link)
.then(() => result.success(true));
})
.catch((error: Error) => result.error(this.toPlatformFlutterError(error, 'local_notification_failed')));
}
private async getNotificationAuthorizationStatus(): Promise<number> {
if (await notificationManager.isNotificationEnabled()) {
return 1;
}
const store = await preferences.getPreferences(this.context, NOTIFICATION_PREFERENCES_NAME);
return store.getSync(NOTIFICATION_AUTH_REQUESTED_KEY, false) as boolean ? -1 : 0;
}
private async markNotificationAuthorizationRequested(): Promise<void> {
const store = await preferences.getPreferences(this.context, NOTIFICATION_PREFERENCES_NAME);
store.putSync(NOTIFICATION_AUTH_REQUESTED_KEY, true);
await store.flush();
}
private async openNotificationSettings(): Promise<void> {
const want: Want = {
abilityName: 'com.huawei.hmos.settings.MainAbility',
bundleName: 'com.huawei.hmos.settings',
uri: 'application_info_entry',
parameters: {
pushParams: this.context.applicationInfo.name,
},
};
await this.context.startAbility(want);
}
private async publishLocalNotification(
notificationId: number,
title: string,
content: string,
link: string,
): Promise<void> {
const notificationWant: Want = {
bundleName: this.context.abilityInfo.bundleName,
abilityName: this.context.abilityInfo.name,
uri: link,
};
const notificationWantAgent = await wantAgent.getWantAgent({
wants: [notificationWant],
actionType: wantAgent.OperationType.START_ABILITY,
requestCode: notificationId,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
});
await notificationManager.publish({
id: notificationId,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: { title, text: content, additionalText: link },
},
wantAgent: notificationWantAgent,
});
}
private async upload(filePath: string, resourceType: HResourceType): Promise<string> {
console.log("------", "uploadFile: " + filePath + " " + resourceType)
if (filePath.length === 0) {
throw new Error('The upload file path is empty.');
}
const config = await this.requestUploadConfig(resourceType);
const obsClient = new ObsClient({
AccessKeyId: config.accessKey,
SecretAccessKey: config.secretKey,
SecurityToken: config.securityToken,
Server: this.normalizeHttpsUrl(config.endpoint),
});
await this.uploadFileWithPostSignature(obsClient, config, filePath, resourceType);
return `${this.normalizeHttpsUrl(config.host).replace(/\/$/, '')}/${config.objectKey}`;
}
private async requestUploadConfig(resourceType: HResourceType): Promise<RequiredUploadConfig> {
if (this.baseUrl.length === 0 || this.accessToken.length === 0) {
throw new Error('A logged-in session is required before uploading a file.');
}
const request = http.createHttp();
try {
const response = await request.request(
`${this.baseUrl.replace(/\/$/, '')}/client/doublefeel/obs/temp_token/`,
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'access_token': this.accessToken,
},
extraData: JSON.stringify({
file_type: resourceType === HResourceType.IMAGE ? 'png' : 'mp4',
scene: 'avatar',
count: 1,
}),
expectDataType: http.HttpDataType.STRING,
},
);
if (response.responseCode < 200 || response.responseCode >= 300) {
throw new Error(`Requesting OBS credentials failed (HTTP ${response.responseCode}).`);
}
const tokenInfo = JSON.parse(response.result as string) as TempTokenResponse;
const accessKey = tokenInfo.token?.access_key;
const secretKey = tokenInfo.token?.secret_key;
const securityToken = tokenInfo.token?.security_token;
const objectKey = tokenInfo.keys?.[0];
if (!accessKey || !secretKey || !securityToken || !objectKey || !tokenInfo.host || !tokenInfo.endpoint ||
!tokenInfo.bucket) {
throw new Error('The OBS credential response is incomplete.');
}
return {
accessKey,
secretKey,
securityToken,
objectKey,
host: tokenInfo.host,
endpoint: tokenInfo.endpoint,
bucket: tokenInfo.bucket,
};
} finally {
request.destroy();
}
}
private normalizeHttpsUrl(value: string): string {
return value.startsWith('http://') || value.startsWith('https://') ? value : `https://${value}`;
}
private toFlutterError(error: Error): FlutterError {
return new FlutterError('obs_upload_failed', 'OBSUploadError', error.message);
}
private toPlatformFlutterError(error: Error, code: string): FlutterError {
return new FlutterError(code, 'PlatformHostApiError', error.message);
}
/**
* Streams a local file through Harmony's upload task. The OBS putObject
* API accepts only ArrayBuffer for binary content, which is unsuitable for
* the feedback flow's 200 MB video limit.
*/
private async uploadFileWithPostSignature(
obsClient: ObsClient,
config: RequiredUploadConfig,
filePath: string,
resourceType: HResourceType,
): Promise<void> {
const stagedFile = await this.stageFileForUpload(filePath);
try {
const contentType = this.contentType(filePath, resourceType);
const signature = obsClient.createPostSignatureSync({
Bucket: config.bucket,
Key: config.objectKey,
Expires: 300,
FormParams: {
'content-type': contentType,
'success_action_status': '200',
},
});
const uploadTask = await request.uploadFile(this.context, {
url: this.formUploadUrl(config),
method: 'POST',
header: {},
files: [{
filename: config.objectKey,
name: 'file',
uri: stagedFile.uri,
type: contentType,
}],
data: [
{ name: 'key', value: config.objectKey },
{ name: 'AccessKeyId', value: config.accessKey },
{ name: 'policy', value: signature.Policy },
{ name: 'signature', value: signature.Signature },
{ name: 'x-obs-security-token', value: config.securityToken },
{ name: 'content-type', value: contentType },
{ name: 'success_action_status', value: '200' },
],
});
await this.waitForUploadTask(uploadTask);
} finally {
if (stagedFile.shouldDelete) {
this.deleteSharedFile(stagedFile.path);
}
}
}
private async stageFileForUpload(filePath: string): Promise<StagedUploadFile> {
const cachePrefix = `${this.context.cacheDir}/`;
if (filePath.startsWith(cachePrefix)) {
return {
path: filePath,
uri: `internal://cache/${filePath.substring(cachePrefix.length)}`,
shouldDelete: false,
};
}
const extensionIndex = filePath.lastIndexOf('.');
const extension = extensionIndex >= 0 ? filePath.substring(extensionIndex) : '';
const path = `${this.context.cacheDir}/obs-upload-${Date.now()}-${Math.floor(Math.random() * 1000000)}${extension}`;
try {
await fileIo.copyFile(filePath, path);
} catch (_error) {
this.deleteSharedFile(path);
// ArkTS only permits Error instances to be thrown. The file API may
// report a platform-specific value, so wrap it in a standard Error.
throw new Error('Failed to stage the file for OBS upload.');
}
return {
path,
uri: `internal://cache/${path.substring(cachePrefix.length)}`,
shouldDelete: true,
};
}
private formUploadUrl(config: RequiredUploadConfig): string {
const endpoint = this.normalizeHttpsUrl(config.endpoint).replace(/\/+$/, '');
const endpointMatch = endpoint.match(/^(https?):\/\/(.+)$/);
if (!endpointMatch) {
throw new Error('The OBS endpoint is invalid.');
}
const scheme = endpointMatch[1];
const host = endpointMatch[2];
return host.startsWith(`${config.bucket}.`)
? `${scheme}://${host}`
: `${scheme}://${config.bucket}.${host}`;
}
private waitForUploadTask(uploadTask: request.UploadTask): Promise<void> {
return new Promise((resolve, reject) => {
uploadTask.on('complete', (taskStates) => {
const taskState = taskStates[0];
// Harmony's UploadTask uses 0 (rather than an HTTP status code) to
// indicate that the task completed successfully.
if (taskState && taskState.responseCode === 0) {
resolve();
return;
}
reject(new Error(`OBS form upload failed (task code ${taskState?.responseCode ?? 'unknown'}).`));
});
uploadTask.on('fail', (taskStates) => {
const taskState = taskStates[0];
reject(new Error(`OBS form upload failed: ${taskState?.message ?? 'unknown error'}.`));
});
});
}
private contentType(filePath: string, resourceType: HResourceType): string {
const lowerPath = filePath.toLowerCase();
if (lowerPath.endsWith('.jpg') || lowerPath.endsWith('.jpeg')) {
return 'image/jpeg';
}
if (lowerPath.endsWith('.heic')) {
return 'image/heic';
}
if (lowerPath.endsWith('.webp')) {
return 'image/webp';
}
if (lowerPath.endsWith('.gif')) {
return 'image/gif';
}
if (lowerPath.endsWith('.mov')) {
return 'video/quicktime';
}
return resourceType === HResourceType.IMAGE ? 'image/png' : 'video/mp4';
}
}
interface RequiredUploadConfig {
accessKey: string;
secretKey: string;
securityToken: string;
objectKey: string;
host: string;
endpoint: string;
bucket: string;
}
interface StagedUploadFile {
path: string;
uri: string;
shouldDelete: boolean;
}
... ...
... ... @@ -16,7 +16,7 @@ import { FlutterError, Result, WeChatHostApi } from '../pigeon/WeChatApi';
* Fill in the AppID created for the HarmonyOS application in WeChat Open
* Platform. Keep AppSecret on the server only.
*/
const WECHAT_APP_ID = '';
const WECHAT_APP_ID = 'wx42b603acf3dd68f5';
/** Bridges the native OpenSDK authorization callback to Flutter. */
export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandler {
... ...
... ... @@ -103,10 +103,16 @@ export class PigeonCodec extends StandardMessageCodec {
}
}
export interface Result<T> {
success(result: T): void;
error(error: Error): void;
}
/* Generated abstract class from Pigeon that represents a handler of messages from Flutter.*/
export abstract class AlipayHostApi {
abstract launchAliPay(prepayData: string): AliPayResultCode;
abstract launchAliPay(prepayData: string, result: Result<AliPayResultCode>): void;
/** The codec used by AlipayHostApi. */
static getCodec(): MessageCodec<Object> {
return PigeonCodec.INSTANCE;
... ... @@ -130,16 +136,18 @@ export abstract class AlipayHostApi {
reply.reply(wrapError(new Error('Invalid Pigeon message: expected at least 1 argument(s).')));
return;
}
let res: Array<Object | null> = [];
try {
let pigeonEnumResult: AliPayResultCode = api!.launchAliPay(args[0] as string);
let output: ESObject = pigeonEnumResult === null || pigeonEnumResult === undefined ? null : new AliPayResultCodeEnum(AliPayResultCode[pigeonEnumResult as number]);
res.push(output);
} catch (error) {
let wrappedError: Array<Object | null> = wrapError(error);
res = wrappedError;
class ResultImp implements Result<AliPayResultCode> {
success(result: AliPayResultCode): void {
let res: Array<Object | null> = [];
res.push(new AliPayResultCodeEnum(AliPayResultCode[result as number]));
reply.reply(res);
}
error(error: Error): void {
reply.reply(wrapError(error));
}
}
reply.reply(res);
api!.launchAliPay(args[0] as string, new ResultImp());
} });
} else {
channel.setMessageHandler(null);
... ...
... ... @@ -139,6 +139,13 @@ export abstract class HealthKitHostApi {
abstract checkHealthAppAuthorization(result: Result<HealthAuthorization>): void;
abstract requestHealthClientAuthorization(result: Result<boolean>): void;
/*
* Requests Huawei Health to upload the user's latest data to its cloud.
*
* This only triggers the Health app's configured device-to-cloud sync; it
* does not wait for, or return, the subsequently available cloud data.
*/
abstract syncHealthDataToCloud(result: Result<boolean>): void;
/** The codec used by HealthKitHostApi. */
static getCodec(): MessageCodec<Object> {
return PigeonCodec.INSTANCE;
... ... @@ -200,5 +207,32 @@ export abstract class HealthKitHostApi {
channel.setMessageHandler(null);
}
}
{
let channel: BasicMessageChannel<Object> =
new BasicMessageChannel(
binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
if (api != null) {
channel.setMessageHandler({
onMessage(message: Object, reply: Reply<Object>) {
class ResultImp implements Result<boolean>{
success(result: boolean): void {
let res: Array<Object | null> = [];
res.push(result);
reply.reply(res);
}
error(error: Error): void {
let wrappedError: Array<Object | null> = wrapError(error);
reply.reply(wrappedError);
}
}
let resultCallback: Result<boolean> = new ResultImp();
api!.syncHealthDataToCloud(resultCallback);
} });
} else {
channel.setMessageHandler(null);
}
}
}
}
... ...
... ... @@ -10,7 +10,9 @@
"querySchemes": [
"huaweischeme",
"weixin",
"wxopensdk"
"wxopensdk",
"https",
"alipays"
],
"deliveryWithInstall": true,
"installationFree": false,
... ...
... ... @@ -10,7 +10,7 @@
},
{
"name": "EntryAbility_label",
"value": "doublefeel_flutter"
"value": "DoubleFeel"
}
]
}
\ No newline at end of file
... ...

6.63 KB | W: | H:

3.86 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
... ... @@ -10,7 +10,7 @@
},
{
"name": "EntryAbility_label",
"value": "doublefeel_flutter"
"value": "DoubleFeel"
}
]
}
\ No newline at end of file
... ...
... ... @@ -10,7 +10,7 @@
},
{
"name": "EntryAbility_label",
"value": "doublefeel_flutter"
"value": "DoubleFeel"
}
]
}
\ No newline at end of file
... ...
{
"modelVersion": "5.0.0",
"name": "doublefeel_flutter",
"version": "1.0.0",
"name": "DoubleFeel",
"version": "2.7.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"@cashier_alipay/cashiersdk": "15.8.43",
"@tencent/wechat_open_sdk": "1.0.21"
},
"devDependencies": {
"@ohos/hypium": "1.0.6"
}
}
},
"dynamicDependencies": {}
}
\ No newline at end of file
... ...
... ... @@ -34,4 +34,11 @@ abstract class HealthKitHostApi {
@async
bool requestHealthClientAuthorization();
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
/// does not wait for, or return, the subsequently available cloud data.
@async
bool syncHealthDataToCloud();
}
... ...
... ... @@ -6,7 +6,7 @@ packages:
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "85.0.0"
analyzer:
... ... @@ -14,7 +14,7 @@ packages:
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.7.1"
archive:
... ... @@ -22,7 +22,7 @@ packages:
description:
name: archive
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.0"
args:
... ... @@ -30,7 +30,7 @@ packages:
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
... ... @@ -38,7 +38,7 @@ packages:
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
boolean_selector:
... ... @@ -46,7 +46,7 @@ packages:
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
build:
... ... @@ -54,7 +54,7 @@ packages:
description:
name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
build_config:
... ... @@ -62,7 +62,7 @@ packages:
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
build_daemon:
... ... @@ -70,7 +70,7 @@ packages:
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.4"
build_resolvers:
... ... @@ -78,7 +78,7 @@ packages:
description:
name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.4"
build_runner:
... ... @@ -86,7 +86,7 @@ packages:
description:
name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.15"
build_runner_core:
... ... @@ -94,7 +94,7 @@ packages:
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.0.0"
built_collection:
... ... @@ -102,7 +102,7 @@ packages:
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
built_value:
... ... @@ -110,7 +110,7 @@ packages:
description:
name: built_value
sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.12.7"
cached_network_image:
... ... @@ -118,7 +118,7 @@ packages:
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
... ... @@ -126,7 +126,7 @@ packages:
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.1"
cached_network_image_web:
... ... @@ -134,7 +134,7 @@ packages:
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
characters:
... ... @@ -142,7 +142,7 @@ packages:
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
checked_yaml:
... ... @@ -150,7 +150,7 @@ packages:
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.3"
clock:
... ... @@ -158,7 +158,7 @@ packages:
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
code_builder:
... ... @@ -166,7 +166,7 @@ packages:
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.10.1"
collection:
... ... @@ -174,7 +174,7 @@ packages:
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
convert:
... ... @@ -182,7 +182,7 @@ packages:
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
cross_file:
... ... @@ -190,7 +190,7 @@ packages:
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4+2"
crypto:
... ... @@ -198,7 +198,7 @@ packages:
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
cupertino_icons:
... ... @@ -206,7 +206,7 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
dart_style:
... ... @@ -214,7 +214,7 @@ packages:
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
dio:
... ... @@ -222,7 +222,7 @@ packages:
description:
name: dio
sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.11.0"
dio_web_adapter:
... ... @@ -230,7 +230,7 @@ packages:
description:
name: dio_web_adapter
sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
equatable:
... ... @@ -238,7 +238,7 @@ packages:
description:
name: equatable
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
fake_async:
... ... @@ -246,7 +246,7 @@ packages:
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
ffi:
... ... @@ -254,7 +254,7 @@ packages:
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
file:
... ... @@ -262,7 +262,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_selector_linux:
... ... @@ -270,7 +270,7 @@ packages:
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+2"
file_selector_macos:
... ... @@ -278,7 +278,7 @@ packages:
description:
name: file_selector_macos
sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.4+3"
file_selector_platform_interface:
... ... @@ -286,7 +286,7 @@ packages:
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.2"
file_selector_windows:
... ... @@ -294,7 +294,7 @@ packages:
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+4"
fixnum:
... ... @@ -302,7 +302,7 @@ packages:
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
fl_chart:
... ... @@ -310,7 +310,7 @@ packages:
description:
name: fl_chart
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.70.2"
flutter:
... ... @@ -323,7 +323,7 @@ packages:
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
flutter_lints:
... ... @@ -331,7 +331,7 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_localizations:
... ... @@ -344,7 +344,7 @@ packages:
description:
name: flutter_plugin_android_lifecycle
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.29"
flutter_test:
... ... @@ -357,7 +357,7 @@ packages:
description:
name: flutter_timezone
sha256: "869677426fde92dbe170fb7d2d4929f2a8343c2f5f62f08b0bb64f908630b073"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.0"
flutter_web_plugins:
... ... @@ -379,7 +379,7 @@ packages:
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
get:
... ... @@ -387,7 +387,7 @@ packages:
description:
name: get
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.7.3"
glob:
... ... @@ -395,7 +395,7 @@ packages:
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
graphs:
... ... @@ -403,7 +403,7 @@ packages:
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
http:
... ... @@ -411,7 +411,7 @@ packages:
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.0"
http_multi_server:
... ... @@ -419,7 +419,7 @@ packages:
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.2"
http_parser:
... ... @@ -427,31 +427,33 @@ packages:
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
image_cropper:
dependency: "direct main"
description:
name: image_cropper
sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
url: "https://pub.dev"
source: hosted
path: image_cropper
ref: "br_v9.1.0_ohos"
resolved-ref: b1b45b1a5333571095569d53d01720c505fff983
url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
source: git
version: "9.1.0"
image_cropper_for_web:
dependency: transitive
description:
name: image_cropper_for_web
sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
url: "https://pub.dev"
source: hosted
path: image_cropper_for_web
ref: "br_v9.1.0_ohos"
resolved-ref: b1b45b1a5333571095569d53d01720c505fff983
url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
source: git
version: "6.1.0"
image_cropper_platform_interface:
dependency: "direct overridden"
description:
name: image_cropper_platform_interface
sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.1.0"
image_picker:
... ... @@ -459,7 +461,7 @@ packages:
description:
name: image_picker
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
image_picker_android:
... ... @@ -467,7 +469,7 @@ packages:
description:
name: image_picker_android
sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13"
image_picker_for_web:
... ... @@ -475,7 +477,7 @@ packages:
description:
name: image_picker_for_web
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
image_picker_ios:
... ... @@ -483,7 +485,7 @@ packages:
description:
name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13"
image_picker_linux:
... ... @@ -491,7 +493,7 @@ packages:
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_picker_macos:
... ... @@ -499,7 +501,7 @@ packages:
description:
name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_picker_ohos:
... ... @@ -516,7 +518,7 @@ packages:
description:
name: image_picker_platform_interface
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
image_picker_windows:
... ... @@ -524,7 +526,7 @@ packages:
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
intl:
... ... @@ -532,7 +534,7 @@ packages:
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.19.0"
io:
... ... @@ -540,7 +542,7 @@ packages:
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
js:
... ... @@ -548,7 +550,7 @@ packages:
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.1"
json_annotation:
... ... @@ -556,7 +558,7 @@ packages:
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.9.0"
leak_tracker:
... ... @@ -564,7 +566,7 @@ packages:
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
... ... @@ -572,7 +574,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.8"
leak_tracker_testing:
... ... @@ -580,7 +582,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
lints:
... ... @@ -588,7 +590,7 @@ packages:
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
logger:
... ... @@ -596,7 +598,7 @@ packages:
description:
name: logger
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
logging:
... ... @@ -604,7 +606,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
lottie:
... ... @@ -612,7 +614,7 @@ packages:
description:
name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.1"
matcher:
... ... @@ -620,7 +622,7 @@ packages:
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
... ... @@ -628,7 +630,7 @@ packages:
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
... ... @@ -636,7 +638,7 @@ packages:
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
mime:
... ... @@ -644,7 +646,7 @@ packages:
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
octo_image:
... ... @@ -652,7 +654,7 @@ packages:
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
package_config:
... ... @@ -660,7 +662,7 @@ packages:
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
path:
... ... @@ -668,7 +670,7 @@ packages:
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
path_provider:
... ... @@ -676,7 +678,7 @@ packages:
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
... ... @@ -684,7 +686,7 @@ packages:
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.17"
path_provider_foundation:
... ... @@ -692,7 +694,7 @@ packages:
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
path_provider_linux:
... ... @@ -700,7 +702,7 @@ packages:
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
... ... @@ -708,7 +710,7 @@ packages:
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
... ... @@ -716,7 +718,7 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
... ... @@ -724,7 +726,7 @@ packages:
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.4.0"
permission_handler_android:
... ... @@ -732,7 +734,7 @@ packages:
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.1.0"
permission_handler_apple:
... ... @@ -740,7 +742,7 @@ packages:
description:
name: permission_handler_apple
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.6.1"
permission_handler_html:
... ... @@ -748,7 +750,7 @@ packages:
description:
name: permission_handler_html
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.4+1"
permission_handler_ohos:
... ... @@ -765,7 +767,7 @@ packages:
description:
name: permission_handler_platform_interface
sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.4.0"
permission_handler_windows:
... ... @@ -773,7 +775,7 @@ packages:
description:
name: permission_handler_windows
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
pigeon:
... ... @@ -790,7 +792,7 @@ packages:
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
... ... @@ -798,7 +800,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
pool:
... ... @@ -806,7 +808,7 @@ packages:
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.2"
posix:
... ... @@ -814,7 +816,7 @@ packages:
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.2"
pretty_dio_logger:
... ... @@ -822,7 +824,7 @@ packages:
description:
name: pretty_dio_logger
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
pub_semver:
... ... @@ -830,7 +832,7 @@ packages:
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
pubspec_parse:
... ... @@ -838,7 +840,7 @@ packages:
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
rxdart:
... ... @@ -846,7 +848,7 @@ packages:
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
share_plus:
... ... @@ -881,7 +883,7 @@ packages:
description:
name: shared_preferences_android
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.11"
shared_preferences_foundation:
... ... @@ -889,7 +891,7 @@ packages:
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.4"
shared_preferences_linux:
... ... @@ -897,7 +899,7 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_ohos:
... ... @@ -914,7 +916,7 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_web:
... ... @@ -922,7 +924,7 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.3"
shared_preferences_windows:
... ... @@ -930,7 +932,7 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shelf:
... ... @@ -938,7 +940,7 @@ packages:
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.2"
shelf_web_socket:
... ... @@ -946,7 +948,7 @@ packages:
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
simple_gesture_detector:
... ... @@ -954,7 +956,7 @@ packages:
description:
name: simple_gesture_detector
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
sky_engine:
... ... @@ -967,7 +969,7 @@ packages:
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
sqflite:
... ... @@ -984,7 +986,7 @@ packages:
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
sqflite_common:
... ... @@ -992,7 +994,7 @@ packages:
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
... ... @@ -1000,7 +1002,7 @@ packages:
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1+1"
sqflite_ohos:
... ... @@ -1017,7 +1019,7 @@ packages:
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace:
... ... @@ -1025,7 +1027,7 @@ packages:
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.0"
stream_channel:
... ... @@ -1033,7 +1035,7 @@ packages:
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
stream_transform:
... ... @@ -1041,7 +1043,7 @@ packages:
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
string_scanner:
... ... @@ -1049,7 +1051,7 @@ packages:
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
synchronized:
... ... @@ -1057,7 +1059,7 @@ packages:
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0+3"
table_calendar:
... ... @@ -1065,7 +1067,7 @@ packages:
description:
name: table_calendar
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
term_glyph:
... ... @@ -1073,7 +1075,7 @@ packages:
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
... ... @@ -1081,7 +1083,7 @@ packages:
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.3"
thinking_analytics:
... ... @@ -1089,7 +1091,7 @@ packages:
description:
name: thinking_analytics
sha256: b01cac0b5482e71c1d75c44c77d27f427662cc65a77b7bc3c8b49617d7a01e02
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.3"
timing:
... ... @@ -1097,7 +1099,7 @@ packages:
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
typed_data:
... ... @@ -1105,7 +1107,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
url_launcher_linux:
... ... @@ -1113,7 +1115,7 @@ packages:
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
... ... @@ -1121,7 +1123,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
url_launcher_web:
... ... @@ -1129,7 +1131,7 @@ packages:
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
url_launcher_windows:
... ... @@ -1137,7 +1139,7 @@ packages:
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
uuid:
... ... @@ -1145,7 +1147,7 @@ packages:
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.6.0"
vector_math:
... ... @@ -1153,7 +1155,7 @@ packages:
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
video_thumbnail:
... ... @@ -1161,7 +1163,7 @@ packages:
description:
name: video_thumbnail
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.6"
vm_service:
... ... @@ -1169,7 +1171,7 @@ packages:
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.0"
watcher:
... ... @@ -1177,7 +1179,7 @@ packages:
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
web:
... ... @@ -1185,7 +1187,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
web_socket:
... ... @@ -1193,7 +1195,7 @@ packages:
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.1"
web_socket_channel:
... ... @@ -1201,7 +1203,7 @@ packages:
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
webview_flutter:
... ... @@ -1254,7 +1256,7 @@ packages:
description:
name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.10.1"
xdg_directories:
... ... @@ -1262,7 +1264,7 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
yaml:
... ... @@ -1270,7 +1272,7 @@ packages:
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
sdks:
... ...
name: doublefeel_flutter
description: "A new Flutter project."
publish_to: 'none'
publish_to: 'none'
version: 2.5.0+100
version: 2.7.0+1000000
environment:
sdk: ^3.6.2
... ... @@ -21,7 +21,7 @@ dependencies:
fl_chart: ^0.70.2
logger: ^2.6.2
webview_flutter:
git:
git:
url: https://gitcode.com/openharmony-tpc/flutter_packages.git
path: packages/webview_flutter/webview_flutter
ref: br_webview_flutter-v4.13.0_ohos
... ... @@ -35,12 +35,12 @@ dependencies:
git:
url: https://gitcode.com/openharmony-sig/flutter_packages.git
path: packages/image_picker/image_picker_ohos
image_cropper: 9.1.0
# image_cropper:
# git:
# url: https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git
# path: ./image_cropper
# ref: br_v9.1.0_ohos
# image_cropper: 9.1.0
image_cropper:
git:
url: https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git
path: ./image_cropper
ref: br_v9.1.0_ohos
permission_handler: ^11.3.1
permission_handler_ohos:
git:
... ...