purchase_view.dart 10.2 KB
import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

import '../controllers/purchase_controller.dart';
import '../widgets/purchase_bottom_bar.dart';
import '../widgets/purchase_colors.dart';
import '../widgets/purchase_plan_card.dart';
import '../widgets/refund_explanation_bottom_sheet.dart';

class PurchaseView extends GetView<PurchaseController> {
  const PurchaseView({super.key});

  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: const SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        statusBarIconBrightness: Brightness.dark,
        statusBarBrightness: Brightness.light,
        systemNavigationBarColor: PurchaseColors.background,
        systemNavigationBarIconBrightness: Brightness.dark,
      ),
      child: Scaffold(
        backgroundColor: PurchaseColors.background,
        body: Stack(
          children: [
            Positioned.fill(
              child: SingleChildScrollView(
                controller: controller.scrollController,
                physics: const ClampingScrollPhysics(),
                padding: const EdgeInsets.only(bottom: 170),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const _HeroHeader(),
                    Padding(
                      padding: const EdgeInsets.only(left: 18, top: 9),
                      child: Text(
                        context.l10n.purchaseHeroTitle,
                        style: const TextStyle(
                          color: Colors.black,
                          fontSize: 24,
                          fontWeight: FontWeight.w600,
                          height: 1.2,
                        ),
                      ),
                    ),
                    const SizedBox(height: 26),
                    _PlanScroller(controller: controller),
                    const SizedBox(height: 22),
                    _SectionTitle(context.l10n.purchaseBenefitsTitle),
                    const SizedBox(height: 14),
                    const PremiumBenefitListView(),
                    const SizedBox(height: 24),
                    _LegalNotes(controller: controller),
                  ],
                ),
              ),
            ),
            Positioned(
              left: 0,
              right: 0,
              top: 0,
              child: _PurchaseNavigationBar(controller: controller),
            ),
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: Obx(
                () => PurchaseBottomBar(
                  label: context.l10n.purchaseUnlockNow,
                  loading: controller.isUnlocking.value,
                  onPressed: controller.unlock,
                  onTermsTap: controller.openUserTerms,
                  onPrivacyTap: controller.openPrivacyPolicy,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _HeroHeader extends StatelessWidget {
  const _HeroHeader();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 320,
      width: double.infinity,
      child: Image.asset(
        R.assetsImagesPurchaseTopBg,
        width: double.infinity,
        height: double.infinity,
        fit: BoxFit.cover,
      ),
    );
  }
}

class _PlanScroller extends StatelessWidget {
  const _PlanScroller({required this.controller});

  final PurchaseController controller;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 123,
      child: Obx(
        () {
          if (controller.isLoadingProducts.value && controller.plans.isEmpty) {
            return const Center(
              child: CircularProgressIndicator(strokeWidth: 2),
            );
          }

          final selectedIndex = controller.selectedIndex.value;
          return ListView.separated(
            controller: controller.planScrollController,
            padding: const EdgeInsets.symmetric(horizontal: 18),
            scrollDirection: Axis.horizontal,
            physics: const ClampingScrollPhysics(),
            itemCount: controller.plans.length,
            separatorBuilder: (_, __) => const SizedBox(width: 12),
            itemBuilder: (context, index) {
              final plan = controller.plans[index];
              return PurchasePlanCard(
                plan: plan,
                selected: selectedIndex == index,
                onTap: () => controller.selectPlan(index),
              );
            },
          );
        },
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Text(
        text,
        style: const TextStyle(
          color: Colors.black,
          fontSize: 18,
          fontWeight: FontWeight.w600,
          height: 1.2,
        ),
      ),
    );
  }
}

class _PurchaseNavigationBar extends StatelessWidget {
  const _PurchaseNavigationBar({required this.controller});

  final PurchaseController controller;

  @override
  Widget build(BuildContext context) {
    final topInset = MediaQuery.paddingOf(context).top;
    return Obx(
      () => AnimatedContainer(
        duration: const Duration(milliseconds: 160),
        height: topInset + 44,
        padding: EdgeInsets.only(top: topInset),
        color: controller.isScrolled.value ? Colors.white : Colors.transparent,
        child: Row(
          children: [
            const SizedBox(width: 4),
            IconButton(
              onPressed: Get.back,
              icon: Image.asset(
                'assets/images/common/ic_nav_back.webp',
                width: 24,
                height: 24,
              ),
            ),
            const Spacer(),
            TextButton(
              onPressed: controller.isRestoring.value
                  ? null
                  : controller.restorePurchase,
              style: TextButton.styleFrom(
                foregroundColor: PurchaseColors.title,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                textStyle: const TextStyle(fontSize: 14, height: 1.2),
              ),
              child: controller.isRestoring.value
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : Text(context.l10n.purchaseRestore),
            ),
          ],
        ),
      ),
    );
  }
}

class _LegalNotes extends StatelessWidget {
  const _LegalNotes({required this.controller});

  final PurchaseController controller;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            context.l10n.purchaseNotesTitle,
            style: const TextStyle(
              color: Colors.black,
              fontSize: 18,
              fontWeight: FontWeight.w600,
              height: 1.2,
            ),
          ),
          const SizedBox(height: 24),
          _LegalText(
            prefix: '1. ',
            text: context.l10n.purchaseNoteSubscription,
            linkText: '了解更多',
            linkSuffix: '。',
            onLinkTap: showRefundExplanationBottomSheet,
          ),
          const SizedBox(height: 12),
          _LegalText(
            prefix: '2. ',
            text: context.l10n.purchaseNoteRestore,
            linkText: context.l10n.purchaseRestore,
            linkSuffix: '。',
            onLinkTap: controller.restorePurchase,
          ),
          const SizedBox(height: 12),
          _LegalText(
            prefix: '3. ',
            text: context.l10n.purchaseNoteContact,
            linkText: '联系我们',
            linkSuffix: '。',
            onLinkTap: controller.openContactUs,
          ),
        ],
      ),
    );
  }
}

class _LegalText extends StatelessWidget {
  const _LegalText({
    required this.prefix,
    required this.text,
    this.linkText,
    this.linkSuffix,
    this.onLinkTap,
  });

  final String prefix;
  final String text;
  final String? linkText;
  final String? linkSuffix;
  final VoidCallback? onLinkTap;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          prefix,
          style: const TextStyle(
            color: PurchaseColors.subtitle,
            fontSize: 12,
            height: 1.4,
          ),
        ),
        Expanded(
          child: Text.rich(
            TextSpan(
              children: [
                TextSpan(text: text),
                if (linkText case final linkText?)
                  WidgetSpan(
                    alignment: PlaceholderAlignment.baseline,
                    baseline: TextBaseline.alphabetic,
                    child: GestureDetector(
                      behavior: HitTestBehavior.opaque,
                      onTap: onLinkTap,
                      child: Text(
                        linkText,
                        style: const TextStyle(
                          color: Color(0xFF845EEE),
                          fontSize: 12,
                          height: 1.4,
                          fontWeight: FontWeight.w500,
                          decoration: TextDecoration.underline,
                          decorationColor: Color(0xFF845EEE),
                        ),
                      ),
                    ),
                  ),
                if (linkSuffix case final linkSuffix?)
                  TextSpan(text: linkSuffix),
              ],
            ),
            style: const TextStyle(
              color: PurchaseColors.subtitle,
              fontSize: 12,
              height: 1.4,
            ),
          ),
        ),
      ],
    );
  }
}