Mitra Profil: WA/TG contacts + Keluar-only (no Hapus Akun)
Replaces the placeholder "Hubungi Koordinator" row with two real
contacts pulled from backend config (support_handles_json), and drops
the "Hapus Akun" CTA. Mirrors the figma BestieProfile design but uses
the same WA/TG channel as the customer Tanya Admin sheet — business
decided the same ops team triages both audiences.
Backend:
- Promote support-handles route from /api/client to /api/shared
(renamed file + export). Both apps now consume the same endpoint;
hitting /api/client/* from mitra would violate the per-app
convention in mitra_app/CLAUDE.md.
- client_app provider updated to /api/shared/support-handles.
Mitra app:
- New support_handles_provider mirroring the client_app one. Adds a
`displayHandle` getter that strips the URL scheme for the subtitle
("https://wa.me/X" → "wa.me/X", "https://t.me/Y" → "t.me/Y") so the
row looks like the figma without exposing raw URLs.
- Profil screen now lists: Chat WhatsApp Kami, Chat Telegram Kami,
Syarat & Ketentuan, Kebijakan Privasi. Danger zone simplified to
Keluar only — mitras request account deletion through the same
WA/TG channels (no separate self-service path).
- url_launcher added as a runtime dep, launches deeplinks in
externalApplication mode with graceful snackbar fallback when
parsing or launching fails.
Updates [[feedback-mitra-internal-audience]] — pre-login rule still
holds (no admin CTAs on S3a/S3b/AccountInactive), but the post-login
Profil tab now does surface WA/TG. Overrides decided 2026-05-21.
Verified on emulator-5556: Profil tab renders both rows with handles
from `wa.me/6285173310010` + `t.me/halobestie`, Keluar present, no
Hapus Akun button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
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`.
|
||||
@@ -10,23 +12,23 @@ import '../../core/theme/widgets/widgets.dart';
|
||||
/// Lives inside branch 2 of the shell (`router.dart`), so `BestieTabBar` is
|
||||
/// rendered by `ShellScreen` — this screen owns body content only.
|
||||
///
|
||||
/// Stage 4 deviation from the JSX: the Figma "Chat WhatsApp Kami / Chat
|
||||
/// Telegram Kami" rows surface customer-facing admin handles. Mitras are
|
||||
/// internal-only audience (see project memory `feedback_mitra_internal_audience`),
|
||||
/// so those two rows are replaced with a single "Hubungi Koordinator" entry
|
||||
/// pointing at the internal coordinator channel.
|
||||
/// 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. Hardcoded
|
||||
// for now to avoid pulling a new dependency.
|
||||
// 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';
|
||||
@@ -45,20 +47,18 @@ class ProfilScreen extends ConsumerWidget {
|
||||
_ProfileCard(displayName: displayName, phone: phone),
|
||||
const SizedBox(height: 28),
|
||||
_MenuList(
|
||||
onTapCoordinator: () => _snack(
|
||||
context,
|
||||
'Hubungi koordinator via grup internal — info lengkap segera tersedia',
|
||||
),
|
||||
handles: handlesAsync.valueOrNull,
|
||||
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),
|
||||
_DangerZone(
|
||||
onLogout: () => _confirmLogout(context, ref),
|
||||
onDelete: () => _snack(
|
||||
context,
|
||||
'Hubungi koordinator untuk penghapusan akun',
|
||||
),
|
||||
HaloButton(
|
||||
label: 'Keluar',
|
||||
fullWidth: true,
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: () => _confirmLogout(context, ref),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _VersionFooter(version: _appVersion),
|
||||
@@ -69,6 +69,22 @@ class ProfilScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -221,27 +237,41 @@ class _ProfileCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Menu list — 3 stacked _MenuTile items ───────────────────────────────
|
||||
// ─── Menu list — WA + Telegram + Terms + Privacy ─────────────────────────
|
||||
class _MenuList extends StatelessWidget {
|
||||
final VoidCallback onTapCoordinator;
|
||||
final SupportHandles? handles;
|
||||
final VoidCallback onTapWa;
|
||||
final VoidCallback onTapTelegram;
|
||||
final VoidCallback onTapTerms;
|
||||
final VoidCallback onTapPrivacy;
|
||||
|
||||
const _MenuList({
|
||||
required this.onTapCoordinator,
|
||||
required this.handles,
|
||||
required this.onTapWa,
|
||||
required this.onTapTelegram,
|
||||
required this.onTapTerms,
|
||||
required this.onTapPrivacy,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final waSub = handles?.wa?.displayHandle ?? '';
|
||||
final tgSub = handles?.telegram?.displayHandle ?? '';
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_MenuTile(
|
||||
icon: Icons.support_agent_outlined,
|
||||
label: 'Hubungi Koordinator',
|
||||
subtitle: 'via grup koordinator internal',
|
||||
onTap: onTapCoordinator,
|
||||
icon: Icons.chat_bubble_outline,
|
||||
label: 'Chat WhatsApp Kami',
|
||||
subtitle: waSub.isEmpty ? null : waSub,
|
||||
onTap: onTapWa,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_MenuTile(
|
||||
icon: Icons.send_outlined,
|
||||
label: 'Chat Telegram Kami',
|
||||
subtitle: tgSub.isEmpty ? null : tgSub,
|
||||
onTap: onTapTelegram,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_MenuTile(
|
||||
@@ -342,73 +372,6 @@ class _MenuTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Danger zone — Keluar (secondary) + Hapus Akun (danger text) ─────────
|
||||
class _DangerZone extends StatelessWidget {
|
||||
final VoidCallback onLogout;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _DangerZone({required this.onLogout, required this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
HaloButton(
|
||||
label: 'Keluar',
|
||||
fullWidth: true,
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: onLogout,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Hapus Akun — destructive ghost-style with danger border + text.
|
||||
Material(
|
||||
color: HaloTokens.surface,
|
||||
borderRadius: HaloRadius.pill,
|
||||
child: InkWell(
|
||||
borderRadius: HaloRadius.pill,
|
||||
onTap: onDelete,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: HaloRadius.pill,
|
||||
border: Border.all(
|
||||
color: HaloTokens.danger.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete_outline,
|
||||
size: 17,
|
||||
color: HaloTokens.danger,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Hapus Akun',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.danger,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Version footer ──────────────────────────────────────────────────────
|
||||
class _VersionFooter extends StatelessWidget {
|
||||
final String version;
|
||||
|
||||
Reference in New Issue
Block a user