Phase 4 Stage 8: returning-user shell + Tanya Admin sheet
Bestie Choice Sheet on home Mulai Curhat CTA. When the user has at least one prior session (bestieHistoryHasItemsProvider hits the chat- sessions history endpoint), the CTA opens a HaloBottomSheet with two cards: 'bestie yang udah kenal' -> /chat/history, 'bestie baru' -> /payment/entry. Empty history -> direct to /payment/entry. Bestie history list visual upgrade: HaloOrb (mitraId seed) + name + last-session date + topic pills + sessions count + ONLINE pill. Backend getCustomerHistory now returns topics, mitra_is_online, sessions_count in a single payload (no per-row presence round-trip). BestieOfflinePopup with two variants (returning | new_) replacing the legacy BestieUnavailableDialog. tanya admin ghost CTA on both variants opens the new TanyaAdminSheet. Stage 5's targeted-wait declined stub + Stage 7's chat-screen 409 stub + searching-screen call site all migrated to the real component. TanyaAdminSheet: HaloBottomSheet with WA + Telegram buttons, deeplinks fetched via supportHandlesProvider (CC-config-driven). url_launcher added to client_app; ios LSApplicationQueriesSchemes covers https/http/whatsapp/tg. Stage 2's OTP-blocked popup hubungi admin SnackBar stub also migrated to TanyaAdminSheet. Dev-only POST /internal/_test/seed-history-session lets Maestro 08 flow seed a history row before exercising the choice sheet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/api/api_client_provider.dart';
|
||||
|
||||
class SupportHandle {
|
||||
final String label;
|
||||
final String deeplink;
|
||||
const SupportHandle({required this.label, required this.deeplink});
|
||||
|
||||
factory SupportHandle.fromJson(Map<String, dynamic> json) =>
|
||||
SupportHandle(
|
||||
label: json['label'] as String? ?? '',
|
||||
deeplink: json['deeplink'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class SupportHandles {
|
||||
final SupportHandle? wa;
|
||||
final SupportHandle? telegram;
|
||||
const SupportHandles({this.wa, this.telegram});
|
||||
|
||||
factory SupportHandles.fromJson(Map<String, dynamic> json) {
|
||||
SupportHandle? parse(dynamic v) =>
|
||||
v is Map<String, dynamic> ? SupportHandle.fromJson(v) : null;
|
||||
return SupportHandles(wa: parse(json['wa']), telegram: parse(json['telegram']));
|
||||
}
|
||||
}
|
||||
|
||||
final supportHandlesProvider = FutureProvider<SupportHandles>((ref) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final response = await api.get('/api/client/support-handles');
|
||||
final data = response['data'] as Map<String, dynamic>? ?? const {};
|
||||
return SupportHandles.fromJson(data);
|
||||
});
|
||||
131
client_app/lib/features/support/widgets/tanya_admin_sheet.dart
Normal file
131
client_app/lib/features/support/widgets/tanya_admin_sheet.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
import '../providers/support_handles_provider.dart';
|
||||
|
||||
/// Phase 4 Stage 8 — Tanya Admin sheet.
|
||||
///
|
||||
/// Reads handles from `supportHandlesProvider` (CC-config-driven) and surfaces
|
||||
/// WA + Telegram deeplinks. Tap → `url_launcher` external app. No webview.
|
||||
class TanyaAdminSheet extends ConsumerWidget {
|
||||
const TanyaAdminSheet({super.key});
|
||||
|
||||
static Future<void> show(BuildContext context) {
|
||||
return HaloBottomSheet.show<void>(
|
||||
context,
|
||||
child: const TanyaAdminSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final handlesAsync = ref.watch(supportHandlesProvider);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'tanya admin',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
const Text(
|
||||
'pilih cara yang paling enak buat kamu — admin bakal balas secepatnya.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s24),
|
||||
handlesAsync.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: HaloSpacing.s24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (_, __) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: HaloSpacing.s16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'gagal mengambil kontak admin. coba lagi sebentar.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13,
|
||||
color: HaloTokens.danger,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
HaloButton(
|
||||
label: 'coba lagi',
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
onPressed: () => ref.invalidate(supportHandlesProvider),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (handles) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (handles.wa != null)
|
||||
HaloButton(
|
||||
label: handles.wa!.label.isEmpty ? 'WhatsApp' : handles.wa!.label,
|
||||
variant: HaloButtonVariant.primary,
|
||||
fullWidth: true,
|
||||
icon: const Icon(Icons.chat_bubble_outline),
|
||||
onPressed: () => _launch(context, handles.wa!.deeplink),
|
||||
),
|
||||
if (handles.wa != null && handles.telegram != null)
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
if (handles.telegram != null)
|
||||
HaloButton(
|
||||
label: handles.telegram!.label.isEmpty
|
||||
? 'Telegram'
|
||||
: handles.telegram!.label,
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
onPressed: () => _launch(context, handles.telegram!.deeplink),
|
||||
),
|
||||
if (handles.wa == null && handles.telegram == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: HaloSpacing.s16),
|
||||
child: Text(
|
||||
'kontak admin belum tersedia. coba lagi nanti ya.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launch(BuildContext context, String deeplink) async {
|
||||
final uri = Uri.tryParse(deeplink);
|
||||
if (uri == null) return;
|
||||
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (!ok && context.mounted) {
|
||||
// Fall back to platform default if external launch refused.
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user