diff --git a/client/lib/core/config/store_compliance.dart b/client/lib/core/config/store_compliance.dart new file mode 100644 index 0000000..c3cc855 --- /dev/null +++ b/client/lib/core/config/store_compliance.dart @@ -0,0 +1,20 @@ +// App Store 合规开关(苹果审核指南 3.1.1 / 3.1.3(b))。 +// +// iOS 包内不得出现第三方支付与任何外部购买引导(在线购买卡、套餐价格 +// 链接、升级按钮、"去官网购买"类文案);兑换码激活与中性客服入口不受限 +// (多平台服务条款:允许使用在别处获得的授权)。 +// 其余平台(macOS/Windows/Android/Web 均不走 App Store 分发)不受影响。 +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; + +/// 测试注入:widget/golden 测试跑在 host 上,借此模拟 iOS 形态。 +@visibleForTesting +bool? debugForceHideExternalPurchaseUi; + +/// 为真时隐藏一切外部购买 UI(iOS App Store 包)。 +bool get hideExternalPurchaseUi => + debugForceHideExternalPurchaseUi ?? (!kIsWeb && Platform.isIOS); + +/// 客服邮箱(授权管理「联系客服」卡、异常兜底提示等处使用)。 +const String supportEmail = 'support@51yanmei.com'; diff --git a/client/lib/screens/settings/license_panel.dart b/client/lib/screens/settings/license_panel.dart index 42c873a..7b25661 100644 --- a/client/lib/screens/settings/license_panel.dart +++ b/client/lib/screens/settings/license_panel.dart @@ -5,8 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../core/auth/auth_state.dart'; import '../../core/config/license_copy.dart'; +import '../../core/config/store_compliance.dart'; import '../../core/utils/web_launch.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/app_dims.g.dart'; @@ -98,32 +101,34 @@ class _LicensePanelState extends ConsumerState { ), const SizedBox(height: 16), // ── 卡2:在线购买 / 续费(后端 purchase 接口 admin only, - // 非管理员引导看官网价格页)── - if (isAdmin) - const SettingsCard( - title: '在线购买 / 续费', - desc: '选择套餐支付宝支付,到账后授权自动续期', - child: PurchaseCard(), - ) - else - SettingsCard( - title: '在线购买 / 续费', - desc: '在线购买仅门店管理员可操作', - child: Row(children: [ - Text('在线购买请联系门店管理员 · ', - style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), - InkWell( - // 免登跳官网(一次性换票 SSO):浏览器落地即已登录,可直接购买 - onTap: () => launchAuthedWebPath(ref, '/#pricing'), - child: Text('查看套餐价格 →', - style: TextStyle( - fontSize: AppDims.fsSm, - fontWeight: FontWeight.w600, - color: t.primary)), - ), - ]), - ), - const SizedBox(height: 16), + // 非管理员引导看官网价格页)。iOS App Store 包整卡不渲染 ── + if (!hideExternalPurchaseUi) ...[ + if (isAdmin) + const SettingsCard( + title: '在线购买 / 续费', + desc: '选择套餐支付宝支付,到账后授权自动续期', + child: PurchaseCard(), + ) + else + SettingsCard( + title: '在线购买 / 续费', + desc: '在线购买仅门店管理员可操作', + child: Row(children: [ + Text('在线购买请联系门店管理员 · ', + style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), + InkWell( + // 免登跳官网(一次性换票 SSO):浏览器落地即已登录,可直接购买 + onTap: () => launchAuthedWebPath(ref, '/#pricing'), + child: Text('查看套餐价格 →', + style: TextStyle( + fontSize: AppDims.fsSm, + fontWeight: FontWeight.w600, + color: t.primary)), + ), + ]), + ), + const SizedBox(height: 16), + ], // ── 卡3:兑换券(原型 .redeem:input + 按钮,max 520)── SettingsCard( title: '兑换券', @@ -175,6 +180,29 @@ class _LicensePanelState extends ConsumerState { ], ), ), + // ── 卡5:联系客服(仅 iOS 形态,补位购买卡;措辞保持中性, + // 不得出现购买/续费引导字样——苹果 3.1.1 红线)── + if (hideExternalPurchaseUi) ...[ + const SizedBox(height: 16), + SettingsCard( + title: '联系客服', + desc: '授权与使用问题可通过邮件联系我们', + child: Row(children: [ + Text('客服邮箱 · ', + style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), + InkWell( + onTap: () => + launchUrl(Uri(scheme: 'mailto', path: supportEmail)), + child: Text(supportEmail, + style: TextStyle( + fontSize: AppDims.fsSm, + fontWeight: FontWeight.w600, + fontFamily: AppFonts.mono, + color: t.primary)), + ), + ]), + ), + ], ], ); } diff --git a/client/test/license_panel_ios_test.dart b/client/test/license_panel_ios_test.dart new file mode 100644 index 0000000..fbc10f2 --- /dev/null +++ b/client/test/license_panel_ios_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jiu_client/core/auth/auth_state.dart'; +import 'package:jiu_client/core/config/store_compliance.dart'; +import 'package:jiu_client/core/theme/themes.dart'; +import 'package:jiu_client/providers/license_provider.dart'; +import 'package:jiu_client/screens/settings/license_panel.dart'; + +/// iOS App Store 合规形态(苹果 3.1.1):授权管理页隐藏一切外部购买 UI, +/// 补位中性「联系客服」卡;其余平台购买卡照旧、无客服卡。 + +class _FakeAuth extends AuthNotifier { + _FakeAuth(String role) { + state = AuthState( + user: AuthUser( + accessToken: 't', + refreshToken: 'r', + id: 1, + username: 'wang', + realName: '王经理', + shopNo: 'S001', + shopId: 1, + role: role), + ); + } +} + +class _FakeLicense extends LicenseNotifier { + @override + Future build() async => LicenseInfo( + id: 1, + type: 'annual', + isActive: true, + maxDevices: 2, + phase: 'active', + expiresAt: DateTime.now().add(const Duration(days: 100)), + ); +} + +Widget _panel({required String role}) => ProviderScope( + overrides: [ + authStateProvider.overrideWith((ref) => _FakeAuth(role)), + licenseProvider.overrideWith(() => _FakeLicense()), + ], + child: MaterialApp( + theme: buildTheme('a'), + home: const Scaffold( + body: SingleChildScrollView(child: LicensePanel()), + ), + ), + ); + +void main() { + tearDown(() => debugForceHideExternalPurchaseUi = null); + + testWidgets('iOS 形态:购买卡与价格链接消失,出现联系客服卡', (tester) async { + debugForceHideExternalPurchaseUi = true; + await tester.pumpWidget(_panel(role: 'operator')); + await tester.pumpAndSettle(); + + expect(find.text('在线购买 / 续费'), findsNothing); + expect(find.textContaining('查看套餐价格'), findsNothing); + expect(find.text('联系客服'), findsOneWidget); + expect(find.text(supportEmail), findsOneWidget); + }); + + testWidgets('非 iOS:购买卡照旧,无联系客服卡', (tester) async { + debugForceHideExternalPurchaseUi = false; + await tester.pumpWidget(_panel(role: 'operator')); + await tester.pumpAndSettle(); + + expect(find.text('在线购买 / 续费'), findsOneWidget); + expect(find.textContaining('查看套餐价格'), findsOneWidget); + expect(find.text('联系客服'), findsNothing); + }); +}