friend_interaction_dialog.dart 10.2 KB
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

enum FriendInteractionAction { miss, stick, punch }

/// Friend-interaction bottom sheet. It accepts independent callbacks so each
/// host page can handle the three actions without coupling to a controller.
class FriendInteractionDialog extends StatelessWidget {
  const FriendInteractionDialog({
    super.key,
    required this.avatar,
    required this.steps,
    this.onAction,
    this.onMiss,
    this.onPoke,
    this.onPunch,
  });

  final ImageProvider? avatar;
  final String steps;

  /// Compatibility callback for callers that prefer one typed action handler.
  final ValueChanged<FriendInteractionAction>? onAction;
  final VoidCallback? onMiss;
  final VoidCallback? onPoke;
  final VoidCallback? onPunch;

  static Future<T?> show<T>(
    BuildContext context, {
    required ImageProvider? avatar,
    required String steps,
    ValueChanged<FriendInteractionAction>? onAction,
    VoidCallback? onMiss,
    VoidCallback? onPoke,
    VoidCallback? onPunch,
  }) {
    return showModalBottomSheet<T>(
      context: context,
      isScrollControlled: true,
      backgroundColor: _surface,
      barrierColor: Colors.black.withValues(alpha: .7),
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
      ),
      builder: (_) => FriendInteractionDialog(
        avatar: avatar,
        steps: steps,
        onAction: onAction,
        onMiss: onMiss,
        onPoke: onPoke,
        onPunch: onPunch,
      ),
    );
  }

  static const _surface = Color(0xFFF5F4FF);

  @override
  Widget build(BuildContext context) {
    final isVip =
        Get.find<UserPreferencesStorage>().preferences.value.vipInfo?.isVip ==
        true;
    return Material(
      color: Colors.transparent,
      child: SafeArea(
        top: false,
        child: SizedBox(
          height: 333,
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              Positioned(
                top: -77,
                left: 22,
                child: Image.asset(
                  'assets/images/interact/ic_friend_interact.png',
                  width: 98,
                  height: 106,
                ),
              ),
              Positioned(
                top: -24,
                left: 28,
                child: Transform.rotate(
                  angle: -0.08,
                  child: Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 18,
                      vertical: 8,
                    ),
                    decoration: BoxDecoration(
                      color: const Color(0xFFA084EF),
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      context.l10n.interactActionSheetInvitation,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 18,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ),
                ),
              ),
              Positioned(
                top: 17,
                right: 28,
                child: IconButton(
                  onPressed: () => Navigator.of(context).pop(),
                  icon: const Icon(Icons.close, color: Color(0xFF845EEE)),
                  iconSize: 20,
                  padding: EdgeInsets.zero,
                  constraints: const BoxConstraints.tightFor(
                    width: 20,
                    height: 20,
                  ),
                ),
              ),
              Positioned(top: 42, left: 0, right: 0, child: _buildAvatar()),
              Positioned(
                top: 143,
                left: 0,
                right: 0,
                child: Text(
                  context.l10n.interactTodaySteps,
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    color: Color(0xFF0F0F11),
                    fontSize: 16,
                  ),
                ),
              ),
              Positioned(
                top: 163,
                left: 0,
                right: 0,
                child: RichText(
                  textAlign: TextAlign.center,
                  text: TextSpan(
                    children: [
                      TextSpan(
                        text: steps,
                        style: const TextStyle(
                          color: Color(0xFF845EEE),
                          fontSize: 28,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                      TextSpan(
                        text: context.l10n.interactStepsUnit,
                        style: const TextStyle(
                          color: Color(0xFF845EEE),
                          fontSize: 16,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              Positioned(
                top: 213,
                left: 0,
                right: 0,
                child: FriendInteractActionButtonsView(
                  isVip: isVip,
                  onMiss: _handleMiss,
                  onPoke: _handlePoke,
                  onPunch: _handlePunch,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildAvatar() => Center(
    child: Container(
      width: 90,
      height: 90,
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white, width: 2),
      ),
      child: avatar == null
          ? const Icon(Icons.person, color: Color(0xFF908B91))
          : Image(image: avatar!, fit: BoxFit.cover),
    ),
  );

  void _handleMiss() {
    onAction?.call(FriendInteractionAction.miss);
    onMiss?.call();
  }

  void _handlePoke() {
    onAction?.call(FriendInteractionAction.stick);
    onPoke?.call();
  }

  void _handlePunch() {
    onAction?.call(FriendInteractionAction.punch);
    onPunch?.call();
  }
}

/// Reusable three-action group for any friend-interaction entry point.
///
/// Miss and Punch are Pro actions. Their callbacks can therefore open a
/// membership flow, while Poke can be handled directly by the host page.
class FriendInteractActionButtonsView extends StatelessWidget {
  const FriendInteractActionButtonsView({
    super.key,
    required this.isVip,
    this.onMiss,
    this.onPoke,
    this.onPunch,
  });

  final bool isVip;
  final VoidCallback? onMiss;
  final VoidCallback? onPoke;
  final VoidCallback? onPunch;

  @override
  Widget build(BuildContext context) => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.end,
    children: [
      _FriendInteractActionButton(
        label: context.l10n.interactActionMiss,
        iconAsset: 'assets/images/interact/ic_friend_interact_miss.png',
        showProBadge: !isVip,
        onTap: isVip ? onMiss : _openPurchase,
      ),
      const SizedBox(width: 12),
      _FriendInteractActionButton(
        label: context.l10n.interactActionStick,
        iconAsset: 'assets/images/interact/ic_friend_interact_poke.png',
        isPrimary: true,
        onTap: onPoke,
      ),
      const SizedBox(width: 12),
      _FriendInteractActionButton(
        label: context.l10n.interactActionPunch,
        iconAsset: 'assets/images/interact/ic_friend_interact_punch.png',
        showProBadge: !isVip,
        onTap: isVip ? onPunch : _openPurchase,
      ),
    ],
  );

  void _openPurchase() => Get.toNamed(Routes.PURCHASE);
}

class _FriendInteractActionButton extends StatelessWidget {
  const _FriendInteractActionButton({
    required this.label,
    required this.iconAsset,
    required this.onTap,
    this.isPrimary = false,
    this.showProBadge = false,
  });

  final String label;
  final String iconAsset;
  final VoidCallback? onTap;
  final bool isPrimary;
  final bool showProBadge;

  @override
  Widget build(BuildContext context) {
    final iconSize = isPrimary ? 68.0 : 52.0;
    final buttonSize = isPrimary ? const Size(116, 78) : const Size(90, 56);
    final buttonTop = isPrimary ? 34.0 : 30.0;

    return SizedBox(
      width: buttonSize.width,
      height: isPrimary ? 112 : 93,
      child: Stack(
        clipBehavior: Clip.none,
        alignment: Alignment.topCenter,
        children: [
          Positioned(
            top: buttonTop,
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                onTap: onTap,
                borderRadius: BorderRadius.circular(isPrimary ? 24 : 16),
                child: Ink(
                  width: buttonSize.width,
                  height: buttonSize.height,
                  decoration: BoxDecoration(
                    color: const Color(0xFF896CDC),
                    borderRadius: BorderRadius.circular(isPrimary ? 24 : 16),
                  ),
                  child: Align(
                    alignment: Alignment.bottomCenter,
                    child: Padding(
                      padding: EdgeInsets.only(bottom: isPrimary ? 12 : 9),
                      child: Text(
                        label,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: isPrimary ? 20 : 16,
                          fontWeight: FontWeight.w600,
                          height: 1,
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
          Image.asset(iconAsset, width: iconSize, height: iconSize),
          if (showProBadge)
            Positioned(
              left: 21,
              top: 43,
              child: Image.asset(
                R.assetsImagesTrendProIcon,
                width: 46,
                height: 13,
                fit: BoxFit.contain,
              ),
            ),
        ],
      ),
    );
  }
}