purchase_view.dart 11.4 KB
import 'package:doublefeel_flutter/app/utils/assets_helper.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';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return PopScope(
        canPop: !controller.isFromOnboard,
        onPopInvokedWithResult: (didPop, _) {
          if (!didPop) controller.onBackPressed();
        },
        child: 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: EdgeInsets.only(
                      bottom: 170 + MediaQuery.paddingOf(context).bottom,
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const _HeroHeader(),
                        Padding(
                          padding: EdgeInsets.only(left: 18, top: 9.h),
                          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.h,
      width: double.infinity,
      child: Image.asset(
        assetCompatPath('assets/images/premium/bg_purchase_top.webp'),
        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;
    final arguments = Get.arguments;
    final showCloseButton = arguments is Map &&
        arguments[PurchaseRouteArgs.showCloseButton] == true;
    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: controller.onBackPressed,
              icon: Image.asset(
                showCloseButton
                    ? R.assetsImagesCloseIcon
                    : R.assetsImagesNavBackIcon,
                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: context.l10n.purchaseLinkLearnMore,
            linkSuffix: '。',
            onLinkTap: controller.openRefundExplanation,
          ),
          const SizedBox(height: 12),
          /*
          _LegalText(
            prefix: '3. ',
            text: context.l10n.purchaseNoteRestore,
            linkText: context.l10n.purchaseRestore,
            linkSuffix: '。',
            onLinkTap: controller.restorePurchase,
          ),
          const SizedBox(height: 12),
          */
          _LegalText(
            prefix: '2. ',
            text: context.l10n.purchaseNoteContact,
            linkText: context.l10n.purchaseLinkContactUs,
            linkSuffix: '。',
            onLinkTap: controller.openContactUs,
          ),
        ],
      ),
    );
  }
}

class _LegalText extends StatefulWidget {
  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
  State<_LegalText> createState() => _LegalTextState();
}

class _LegalTextState extends State<_LegalText> {
  late final TapGestureRecognizer _linkRecognizer;

  @override
  void initState() {
    super.initState();
    _linkRecognizer = TapGestureRecognizer()..onTap = widget.onLinkTap;
  }

  @override
  void didUpdateWidget(covariant _LegalText oldWidget) {
    super.didUpdateWidget(oldWidget);
    _linkRecognizer.onTap = widget.onLinkTap;
  }

  @override
  void dispose() {
    _linkRecognizer.dispose();
    super.dispose();
  }

  static const _baseStyle = TextStyle(
    color: PurchaseColors.subtitle,
    fontSize: 12,
    height: 1.4,
  );

  static const _linkStyle = TextStyle(
    color: Color(0xFF845EEE),
    fontWeight: FontWeight.w500,
    decoration: TextDecoration.underline,
    decorationColor: Color(0xFF845EEE),
  );

  TextSpan _buildTextSpan() => TextSpan(
        style: _baseStyle,
        children: [
          TextSpan(text: widget.text),
          if (widget.linkText case final linkText?)
            TextSpan(
              text: linkText,
              style: _linkStyle,
              recognizer: widget.onLinkTap == null ? null : _linkRecognizer,
            ),
          if (widget.linkSuffix case final linkSuffix?)
            TextSpan(text: linkSuffix),
        ],
      );

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          widget.prefix,
          style: const TextStyle(
            color: PurchaseColors.subtitle,
            fontSize: 12,
            height: 1.4,
          ),
        ),
        Expanded(
          child: Text.rich(
            _buildTextSpan(),
            textScaler: MediaQuery.textScalerOf(context),
          ),
        ),
      ],
    );
  }
}