Files
halobestie-clone/mitra_app/lib/features/profile/profil_screen.dart
Ramadhan Sjamsani 9fa4724b2a Mitra Profil: WhatsApp + Telegram brand glyphs
Replaces the generic chat_bubble + send Material icons with the
official WhatsApp + Telegram glyphs from font_awesome_flutter. Adds
the package as a runtime dep; FA brand glyphs are CC BY 4.0 and the
package itself is MIT.

Visual style is kept consistent with the other rows (pink-soft tile
backing, brand-pink glyph fill) rather than full-brand colors —
matches the figma's monochrome tile pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:36:16 +08:00

387 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/auth/auth_notifier.dart';
import '../../core/theme/halo_tokens.dart';
import '../../core/theme/widgets/widgets.dart';
import 'support_handles_provider.dart';
/// Bestie Profil tab — mirrors
/// `figma-bestie/project/screens/v5.jsx::BestieProfile`.
///
/// Lives inside branch 2 of the shell (`router.dart`), so `BestieTabBar` is
/// rendered by `ShellScreen` — this screen owns body content only.
///
/// Menu items mirror the figma: Chat WhatsApp Kami + Chat Telegram Kami
/// (both deep-link via `support_handles_json` from backend config), then
/// Syarat & Ketentuan + Kebijakan Privasi (TODO content). Danger zone is
/// Keluar (logout) only — account deletion is intentionally not exposed
/// in-app; mitras request deletion via the same WA/TG channels.
class ProfilScreen extends ConsumerWidget {
const ProfilScreen({super.key});
// TODO(stage-4): replace with `PackageInfo.fromPlatform().version` when
// the `package_info_plus` package is added in a future change.
static const String _appVersion = '1.0.0';
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(mitraAuthProvider);
final authData = authState.valueOrNull;
final handlesAsync = ref.watch(supportHandlesProvider);
final profile = authData is MitraAuthAuthenticatedData ? authData.profile : null;
final displayName = (profile?['display_name'] as String?) ?? 'Bestie';
final phone = (profile?['phone'] as String?) ?? '';
return Scaffold(
backgroundColor: HaloTokens.bg,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _Header(),
const SizedBox(height: 20),
_ProfileCard(displayName: displayName, phone: phone),
const SizedBox(height: 28),
_MenuList(
onTapWa: () => _launchHandle(context, handlesAsync.valueOrNull?.wa),
onTapTelegram: () => _launchHandle(context, handlesAsync.valueOrNull?.telegram),
onTapTerms: () => _snack(context, 'Segera tersedia'),
onTapPrivacy: () => _snack(context, 'Segera tersedia'),
),
const SizedBox(height: 24),
HaloButton(
label: 'Keluar',
fullWidth: true,
variant: HaloButtonVariant.secondary,
onPressed: () => _confirmLogout(context, ref),
),
const SizedBox(height: 16),
const _VersionFooter(version: _appVersion),
],
),
),
),
);
}
Future<void> _launchHandle(BuildContext context, SupportHandle? handle) async {
if (handle == null || handle.deeplink.isEmpty) {
_snack(context, 'Kontak belum tersedia');
return;
}
final uri = Uri.tryParse(handle.deeplink);
if (uri == null) {
_snack(context, 'Tautan tidak valid');
return;
}
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && context.mounted) {
_snack(context, 'Gagal membuka ${handle.label}');
}
}
void _snack(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message,
style: const TextStyle(fontFamily: HaloTokens.fontBody),
),
behavior: SnackBarBehavior.floating,
),
);
}
Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (ctx) => AlertDialog(
title: const Text(
'Yakin mau keluar?',
style: TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
),
),
content: const Text(
'Kamu bakal sign-out dari akun mitra ini. Login lagi pakai nomor HP yang sama untuk masuk.',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13.5,
height: 1.4,
color: HaloTokens.inkSoft,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text(
'Batal',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
color: HaloTokens.inkSoft,
fontWeight: FontWeight.w600,
),
),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text(
'Keluar',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
color: HaloTokens.danger,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
if (confirmed == true) {
await ref.read(mitraAuthProvider.notifier).logout();
// Router redirect handles navigation to /login on auth state change.
}
}
}
// ─── Header — centered "Profil" title (no back arrow; tab nav owns nav) ──
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Profil',
style: TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontSize: 22,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
letterSpacing: -0.4,
),
),
);
}
}
// ─── Profile card — HaloOrb + display name + role + phone ────────────────
class _ProfileCard extends StatelessWidget {
final String displayName;
final String phone;
const _ProfileCard({required this.displayName, required this.phone});
@override
Widget build(BuildContext context) {
// Deterministic seed from phone — same number always gets the same orb.
final seed = phone.isEmpty ? 0 : phone.hashCode;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
decoration: BoxDecoration(
color: HaloTokens.surface,
borderRadius: HaloRadius.lg,
border: Border.all(color: HaloTokens.border),
boxShadow: HaloShadows.soft,
),
child: Column(
children: [
HaloOrb(size: 96, seed: seed),
const SizedBox(height: 16),
Text(
displayName,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontSize: 24,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
letterSpacing: -0.4,
),
),
const SizedBox(height: 4),
const Text(
'Bestie · Mitra Halo Bestie',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 13,
color: HaloTokens.inkSoft,
),
),
if (phone.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
phone,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: HaloTokens.fontMono,
fontSize: 12.5,
color: HaloTokens.inkMuted,
),
),
],
],
),
);
}
}
// ─── Menu list — WA + Telegram + Terms + Privacy ─────────────────────────
class _MenuList extends StatelessWidget {
final VoidCallback onTapWa;
final VoidCallback onTapTelegram;
final VoidCallback onTapTerms;
final VoidCallback onTapPrivacy;
const _MenuList({
required this.onTapWa,
required this.onTapTelegram,
required this.onTapTerms,
required this.onTapPrivacy,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
_MenuTile(
icon: FontAwesomeIcons.whatsapp,
label: 'Chat WhatsApp Kami',
onTap: onTapWa,
),
const SizedBox(height: 10),
_MenuTile(
icon: FontAwesomeIcons.telegram,
label: 'Chat Telegram Kami',
onTap: onTapTelegram,
),
const SizedBox(height: 10),
_MenuTile(
icon: Icons.description_outlined,
label: 'Syarat & Ketentuan',
onTap: onTapTerms,
),
const SizedBox(height: 10),
_MenuTile(
icon: Icons.lock_outline,
label: 'Kebijakan Privasi',
onTap: onTapPrivacy,
),
],
);
}
}
class _MenuTile extends StatelessWidget {
final IconData icon;
final String label;
final String? subtitle;
final VoidCallback onTap;
const _MenuTile({
required this.icon,
required this.label,
this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: HaloTokens.surface,
borderRadius: HaloRadius.md,
child: InkWell(
borderRadius: HaloRadius.md,
onTap: onTap,
child: Container(
constraints: const BoxConstraints(minHeight: 56),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
borderRadius: HaloRadius.md,
border: Border.all(color: HaloTokens.border),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: HaloTokens.brandSofter,
borderRadius: HaloRadius.sm,
),
alignment: Alignment.center,
child: Icon(icon, size: 18, color: HaloTokens.brandDark),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 14,
fontWeight: FontWeight.w500,
color: HaloTokens.ink,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 11.5,
color: HaloTokens.inkMuted,
),
),
],
],
),
),
const Icon(
Icons.chevron_right,
size: 20,
color: HaloTokens.inkMuted,
),
],
),
),
),
);
}
}
// ─── Version footer ──────────────────────────────────────────────────────
class _VersionFooter extends StatelessWidget {
final String version;
const _VersionFooter({required this.version});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'HaloBestie · v$version',
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 11,
color: HaloTokens.inkMuted,
),
),
);
}
}