diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index 08e8819..a6b1e8d 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -36,6 +36,7 @@ jobs: --tag "$IMAGE" \ --build-arg MAPBOX_ACCESS_TOKEN="${{ secrets.MAPBOX_ACCESS_TOKEN }}" \ --build-arg MAPBOX_STYLE="mapbox/streets-v12" \ + --build-arg TELEGRAM_BOT_URL="https://t.me/carfteebot" \ . - name: Skip stale deployment diff --git a/Dockerfile b/Dockerfile index 8bfd187..b4b7af7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,14 @@ FROM ghcr.io/cirruslabs/flutter:stable AS build WORKDIR /app ARG MAPBOX_ACCESS_TOKEN="" ARG MAPBOX_STYLE="mapbox/streets-v12" +ARG TELEGRAM_BOT_URL="https://t.me/carfteebot" COPY pubspec.* ./ RUN flutter pub get COPY . . RUN flutter build web --release \ --dart-define=MAPBOX_ACCESS_TOKEN="$MAPBOX_ACCESS_TOKEN" \ - --dart-define=MAPBOX_STYLE="$MAPBOX_STYLE" + --dart-define=MAPBOX_STYLE="$MAPBOX_STYLE" \ + --dart-define=TELEGRAM_BOT_URL="$TELEGRAM_BOT_URL" FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/lib/auth/telegram_launcher.dart b/lib/auth/telegram_launcher.dart new file mode 100644 index 0000000..d3c0f07 --- /dev/null +++ b/lib/auth/telegram_launcher.dart @@ -0,0 +1,2 @@ +export 'telegram_launcher_stub.dart' + if (dart.library.js_interop) 'telegram_launcher_web.dart'; diff --git a/lib/auth/telegram_launcher_stub.dart b/lib/auth/telegram_launcher_stub.dart new file mode 100644 index 0000000..87987db --- /dev/null +++ b/lib/auth/telegram_launcher_stub.dart @@ -0,0 +1 @@ +void openTelegramUrl(String url) {} diff --git a/lib/auth/telegram_launcher_web.dart b/lib/auth/telegram_launcher_web.dart new file mode 100644 index 0000000..6ad6cac --- /dev/null +++ b/lib/auth/telegram_launcher_web.dart @@ -0,0 +1,8 @@ +import 'dart:js_interop'; + +@JS('window.open') +external JSAny? _open(JSString url, JSString target, JSString features); + +void openTelegramUrl(String url) { + _open(url.toJS, '_blank'.toJS, 'noopener,noreferrer'.toJS); +} diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index e09d803..73ad0d2 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -5,6 +5,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; +import '../auth/telegram_launcher.dart'; import '../models/place_models.dart'; import '../state/place_controller.dart'; @@ -13,6 +14,10 @@ const _mapboxStyle = String.fromEnvironment( 'MAPBOX_STYLE', defaultValue: 'mapbox/streets-v12', ); +const _telegramBotUrl = String.fromEnvironment( + 'TELEGRAM_BOT_URL', + defaultValue: 'https://t.me/carfteebot', +); class MapflowShell extends ConsumerWidget { const MapflowShell({super.key}); @@ -22,7 +27,12 @@ class MapflowShell extends ConsumerWidget { final asyncState = ref.watch(placeControllerProvider); return asyncState.when( - data: (state) => _MapContent(state: state), + data: (state) => state.hasTelegramAuth + ? _MapContent(state: state) + : _TelegramAuthGate( + onOpenTelegram: () => openTelegramUrl(_telegramBotUrl), + onRetry: () => ref.invalidate(placeControllerProvider), + ), loading: () => const _MapLoading(), error: (error, _) => _MapError(message: error.toString()), ); @@ -119,6 +129,71 @@ class _MapContent extends ConsumerWidget { } } +class _TelegramAuthGate extends StatelessWidget { + const _TelegramAuthGate({ + required this.onOpenTelegram, + required this.onRetry, + }); + + final VoidCallback onOpenTelegram; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + FlutterMap( + options: const MapOptions( + initialCenter: LatLng(10.7718, 106.6982), + initialZoom: 14.2, + ), + children: [const _BaseMapTileLayer(), const _MapAttribution()], + ), + ColoredBox(color: Colors.black.withValues(alpha: 0.18)), + SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFFFFBF5), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Открой через Telegram, чтобы продолжить.', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: 0, + ), + ), + const SizedBox(height: 12), + FilledButton.icon( + onPressed: onOpenTelegram, + icon: const Icon(Icons.telegram), + label: const Text('Telegram'), + ), + TextButton( + onPressed: onRetry, + child: const Text('Обновить'), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + class _MapLoading extends StatelessWidget { const _MapLoading(); @@ -132,10 +207,7 @@ class _MapLoading extends StatelessWidget { initialCenter: LatLng(10.7718, 106.6982), initialZoom: 14.2, ), - children: [ - const _BaseMapTileLayer(), - const _MapAttribution(), - ], + children: [const _BaseMapTileLayer(), const _MapAttribution()], ), const Center(child: CircularProgressIndicator()), ], @@ -159,10 +231,7 @@ class _MapError extends StatelessWidget { initialCenter: LatLng(10.7718, 106.6982), initialZoom: 14.2, ), - children: [ - const _BaseMapTileLayer(), - const _MapAttribution(), - ], + children: [const _BaseMapTileLayer(), const _MapAttribution()], ), SafeArea( child: Align( @@ -218,9 +287,7 @@ class _MapAttribution extends StatelessWidget { Widget build(BuildContext context) { if (_mapboxAccessToken.isEmpty) { return const RichAttributionWidget( - attributions: [ - TextSourceAttribution('OpenStreetMap contributors'), - ], + attributions: [TextSourceAttribution('OpenStreetMap contributors')], ); } diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart index 45f3983..aa5d95c 100644 --- a/lib/state/place_controller.dart +++ b/lib/state/place_controller.dart @@ -67,6 +67,22 @@ class PlaceController extends AsyncNotifier { @override Future build() async { + if (!_api.hasTelegramAuth) { + return const PlaceState( + selectedTrait: PlaceTrait.calm, + places: [], + selectedPlaceId: null, + currentUser: null, + hasTelegramAuth: false, + reviewDraft: VoiceReviewDraft( + placeName: '', + duration: Duration.zero, + extractedTraits: {}, + evidence: [], + ), + ); + } + final currentUser = await _api.authenticateTelegram(); final places = await _api.fetchPlaces(); return PlaceState(