Files
halobestie-clone/mitra_app/lib/features/profile/profil_screen.dart
Ramadhan Sjamsani 31da57d218 Mitra Profil: drop handle subtitle on WA/TG rows
User feedback — the wa.me/... and t.me/... subtitles under "Chat
WhatsApp Kami" / "Chat Telegram Kami" leaked the raw URL into the UI.
Just the label now, matching how typical "contact us" menu entries
read. Tap still launches the deeplink from backend config.

Drop the unused `SupportHandle.displayHandle` getter that produced the
scheme-stripped subtitle — no other call site.

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

386 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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: Icons.chat_bubble_outline,
label: 'Chat WhatsApp Kami',
onTap: onTapWa,
),
const SizedBox(height: 10),
_MenuTile(
icon: Icons.send_outlined,
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,
),
),
);
}
}