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>
387 lines
12 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|