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:
@@ -13,7 +13,7 @@ import { clientPaymentRoutes } from './routes/public/client.payment.routes.js'
|
|||||||
import { clientMitraAvailabilityRoutes } from './routes/public/client.mitra-availability.routes.js'
|
import { clientMitraAvailabilityRoutes } from './routes/public/client.mitra-availability.routes.js'
|
||||||
import { publicBestieAvailabilityRoutes } from './routes/public/public.bestie-availability.routes.js'
|
import { publicBestieAvailabilityRoutes } from './routes/public/public.bestie-availability.routes.js'
|
||||||
import { clientOnboardingRoutes } from './routes/public/client.onboarding.routes.js'
|
import { clientOnboardingRoutes } from './routes/public/client.onboarding.routes.js'
|
||||||
import { clientSupportRoutes } from './routes/public/client.support.routes.js'
|
import { sharedSupportRoutes } from './routes/public/shared.support.routes.js'
|
||||||
import { sharedChatRoutes } from './routes/public/shared.chat.routes.js'
|
import { sharedChatRoutes } from './routes/public/shared.chat.routes.js'
|
||||||
import { errorHandler } from './plugins/error-handler.js'
|
import { errorHandler } from './plugins/error-handler.js'
|
||||||
import { registerWebSocketPlugin, registerWebSocketRoute } from './plugins/websocket.js'
|
import { registerWebSocketPlugin, registerWebSocketRoute } from './plugins/websocket.js'
|
||||||
@@ -38,10 +38,10 @@ export const buildPublicApp = async () => {
|
|||||||
app.register(clientPaymentRoutes, { prefix: '/api/client/payment-sessions' })
|
app.register(clientPaymentRoutes, { prefix: '/api/client/payment-sessions' })
|
||||||
app.register(clientMitraAvailabilityRoutes, { prefix: '/api/client/mitra-availability' })
|
app.register(clientMitraAvailabilityRoutes, { prefix: '/api/client/mitra-availability' })
|
||||||
app.register(publicBestieAvailabilityRoutes, { prefix: '/api/public/bestie' })
|
app.register(publicBestieAvailabilityRoutes, { prefix: '/api/public/bestie' })
|
||||||
// Phase 4: onboarding-state + support handles. Both are tiny so they live in their
|
// Onboarding-state stays client-only (anonymous customer flow). Support
|
||||||
// own files rather than bloating client.auth.routes / shared.config.routes.
|
// handles are shared — both client and mitra apps link the same WA/TG.
|
||||||
app.register(clientOnboardingRoutes, { prefix: '/api/client' })
|
app.register(clientOnboardingRoutes, { prefix: '/api/client' })
|
||||||
app.register(clientSupportRoutes, { prefix: '/api/client' })
|
app.register(sharedSupportRoutes, { prefix: '/api/shared' })
|
||||||
|
|
||||||
// WebSocket route (registered at app level, not prefixed)
|
// WebSocket route (registered at app level, not prefixed)
|
||||||
registerWebSocketRoute(app)
|
registerWebSocketRoute(app)
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import { authenticate } from '../../plugins/auth.js'
|
|||||||
import { getSupportHandles } from '../../services/config.service.js'
|
import { getSupportHandles } from '../../services/config.service.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phase 4 — Tanya Admin sheet handles. Sourced from `app_config.support_handles_json`,
|
* Support channels (WA + Telegram). Sourced from `app_config.support_handles_json`,
|
||||||
* editable by CC. Authenticated so unauthenticated callers can't enumerate the
|
* editable by CC. Authenticated so unauthenticated callers can't enumerate the
|
||||||
* support channels (rate-limit hardening, not a secret).
|
* channels (rate-limit hardening, not a secret).
|
||||||
|
*
|
||||||
|
* Originally registered under /api/client (Phase 4 Tanya Admin sheet). Promoted
|
||||||
|
* to /api/shared when the mitra Profil screen started linking the same WA/TG
|
||||||
|
* contacts — same data, both audiences.
|
||||||
*/
|
*/
|
||||||
export const clientSupportRoutes = async (app) => {
|
export const sharedSupportRoutes = async (app) => {
|
||||||
app.get('/support-handles', { preHandler: authenticate }, async (_request, reply) => {
|
app.get('/support-handles', { preHandler: authenticate }, async (_request, reply) => {
|
||||||
const handles = await getSupportHandles()
|
const handles = await getSupportHandles()
|
||||||
return reply.send({ success: true, data: handles })
|
return reply.send({ success: true, data: handles })
|
||||||
@@ -27,7 +27,7 @@ class SupportHandles {
|
|||||||
|
|
||||||
final supportHandlesProvider = FutureProvider<SupportHandles>((ref) async {
|
final supportHandlesProvider = FutureProvider<SupportHandles>((ref) async {
|
||||||
final api = ref.read(apiClientProvider);
|
final api = ref.read(apiClientProvider);
|
||||||
final response = await api.get('/api/client/support-handles');
|
final response = await api.get('/api/shared/support-handles');
|
||||||
final data = response['data'] as Map<String, dynamic>? ?? const {};
|
final data = response['data'] as Map<String, dynamic>? ?? const {};
|
||||||
return SupportHandles.fromJson(data);
|
return SupportHandles.fromJson(data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../core/auth/auth_notifier.dart';
|
import '../../core/auth/auth_notifier.dart';
|
||||||
import '../../core/theme/halo_tokens.dart';
|
import '../../core/theme/halo_tokens.dart';
|
||||||
import '../../core/theme/widgets/widgets.dart';
|
import '../../core/theme/widgets/widgets.dart';
|
||||||
|
import 'support_handles_provider.dart';
|
||||||
|
|
||||||
/// Bestie Profil tab — mirrors
|
/// Bestie Profil tab — mirrors
|
||||||
/// `figma-bestie/project/screens/v5.jsx::BestieProfile`.
|
/// `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
|
/// Lives inside branch 2 of the shell (`router.dart`), so `BestieTabBar` is
|
||||||
/// rendered by `ShellScreen` — this screen owns body content only.
|
/// rendered by `ShellScreen` — this screen owns body content only.
|
||||||
///
|
///
|
||||||
/// Stage 4 deviation from the JSX: the Figma "Chat WhatsApp Kami / Chat
|
/// Menu items mirror the figma: Chat WhatsApp Kami + Chat Telegram Kami
|
||||||
/// Telegram Kami" rows surface customer-facing admin handles. Mitras are
|
/// (both deep-link via `support_handles_json` from backend config), then
|
||||||
/// internal-only audience (see project memory `feedback_mitra_internal_audience`),
|
/// Syarat & Ketentuan + Kebijakan Privasi (TODO content). Danger zone is
|
||||||
/// so those two rows are replaced with a single "Hubungi Koordinator" entry
|
/// Keluar (logout) only — account deletion is intentionally not exposed
|
||||||
/// pointing at the internal coordinator channel.
|
/// in-app; mitras request deletion via the same WA/TG channels.
|
||||||
class ProfilScreen extends ConsumerWidget {
|
class ProfilScreen extends ConsumerWidget {
|
||||||
const ProfilScreen({super.key});
|
const ProfilScreen({super.key});
|
||||||
|
|
||||||
// TODO(stage-4): replace with `PackageInfo.fromPlatform().version` when
|
// TODO(stage-4): replace with `PackageInfo.fromPlatform().version` when
|
||||||
// the `package_info_plus` package is added in a future change. Hardcoded
|
// the `package_info_plus` package is added in a future change.
|
||||||
// for now to avoid pulling a new dependency.
|
|
||||||
static const String _appVersion = '1.0.0';
|
static const String _appVersion = '1.0.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final authState = ref.watch(mitraAuthProvider);
|
final authState = ref.watch(mitraAuthProvider);
|
||||||
final authData = authState.valueOrNull;
|
final authData = authState.valueOrNull;
|
||||||
|
final handlesAsync = ref.watch(supportHandlesProvider);
|
||||||
|
|
||||||
final profile = authData is MitraAuthAuthenticatedData ? authData.profile : null;
|
final profile = authData is MitraAuthAuthenticatedData ? authData.profile : null;
|
||||||
final displayName = (profile?['display_name'] as String?) ?? 'Bestie';
|
final displayName = (profile?['display_name'] as String?) ?? 'Bestie';
|
||||||
@@ -45,20 +47,18 @@ class ProfilScreen extends ConsumerWidget {
|
|||||||
_ProfileCard(displayName: displayName, phone: phone),
|
_ProfileCard(displayName: displayName, phone: phone),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_MenuList(
|
_MenuList(
|
||||||
onTapCoordinator: () => _snack(
|
handles: handlesAsync.valueOrNull,
|
||||||
context,
|
onTapWa: () => _launchHandle(context, handlesAsync.valueOrNull?.wa),
|
||||||
'Hubungi koordinator via grup internal — info lengkap segera tersedia',
|
onTapTelegram: () => _launchHandle(context, handlesAsync.valueOrNull?.telegram),
|
||||||
),
|
|
||||||
onTapTerms: () => _snack(context, 'Segera tersedia'),
|
onTapTerms: () => _snack(context, 'Segera tersedia'),
|
||||||
onTapPrivacy: () => _snack(context, 'Segera tersedia'),
|
onTapPrivacy: () => _snack(context, 'Segera tersedia'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_DangerZone(
|
HaloButton(
|
||||||
onLogout: () => _confirmLogout(context, ref),
|
label: 'Keluar',
|
||||||
onDelete: () => _snack(
|
fullWidth: true,
|
||||||
context,
|
variant: HaloButtonVariant.secondary,
|
||||||
'Hubungi koordinator untuk penghapusan akun',
|
onPressed: () => _confirmLogout(context, ref),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const _VersionFooter(version: _appVersion),
|
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) {
|
void _snack(BuildContext context, String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
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 {
|
class _MenuList extends StatelessWidget {
|
||||||
final VoidCallback onTapCoordinator;
|
final SupportHandles? handles;
|
||||||
|
final VoidCallback onTapWa;
|
||||||
|
final VoidCallback onTapTelegram;
|
||||||
final VoidCallback onTapTerms;
|
final VoidCallback onTapTerms;
|
||||||
final VoidCallback onTapPrivacy;
|
final VoidCallback onTapPrivacy;
|
||||||
|
|
||||||
const _MenuList({
|
const _MenuList({
|
||||||
required this.onTapCoordinator,
|
required this.handles,
|
||||||
|
required this.onTapWa,
|
||||||
|
required this.onTapTelegram,
|
||||||
required this.onTapTerms,
|
required this.onTapTerms,
|
||||||
required this.onTapPrivacy,
|
required this.onTapPrivacy,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final waSub = handles?.wa?.displayHandle ?? '';
|
||||||
|
final tgSub = handles?.telegram?.displayHandle ?? '';
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_MenuTile(
|
_MenuTile(
|
||||||
icon: Icons.support_agent_outlined,
|
icon: Icons.chat_bubble_outline,
|
||||||
label: 'Hubungi Koordinator',
|
label: 'Chat WhatsApp Kami',
|
||||||
subtitle: 'via grup koordinator internal',
|
subtitle: waSub.isEmpty ? null : waSub,
|
||||||
onTap: onTapCoordinator,
|
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),
|
const SizedBox(height: 10),
|
||||||
_MenuTile(
|
_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 ──────────────────────────────────────────────────────
|
// ─── Version footer ──────────────────────────────────────────────────────
|
||||||
class _VersionFooter extends StatelessWidget {
|
class _VersionFooter extends StatelessWidget {
|
||||||
final String version;
|
final String version;
|
||||||
|
|||||||
43
mitra_app/lib/features/profile/support_handles_provider.dart
Normal file
43
mitra_app/lib/features/profile/support_handles_provider.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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? ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Display form for the menu subtitle: strip the URL scheme so
|
||||||
|
/// "https://wa.me/6285173310010" → "wa.me/6285173310010" and
|
||||||
|
/// "https://t.me/halobestie" → "t.me/halobestie". Falls back to the
|
||||||
|
/// raw deeplink when there's no scheme to strip.
|
||||||
|
String get displayHandle {
|
||||||
|
if (deeplink.isEmpty) return '';
|
||||||
|
final stripped = deeplink.replaceFirst(RegExp(r'^https?://'), '');
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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/shared/support-handles');
|
||||||
|
final data = response['data'] as Map<String, dynamic>? ?? const {};
|
||||||
|
return SupportHandles.fromJson(data);
|
||||||
|
});
|
||||||
@@ -1004,6 +1004,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.30"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ dependencies:
|
|||||||
# Navigation
|
# Navigation
|
||||||
go_router: ^13.2.1
|
go_router: ^13.2.1
|
||||||
flutter_local_notifications: ^21.0.0
|
flutter_local_notifications: ^21.0.0
|
||||||
|
url_launcher: ^6.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user