fix: misselanious updates
This commit is contained in:
@@ -319,6 +319,15 @@
|
|||||||
"deviceRegistered": "Device registered!",
|
"deviceRegistered": "Device registered!",
|
||||||
"openTabsWillSync": "Open tabs will sync to the dashboard.",
|
"openTabsWillSync": "Open tabs will sync to the dashboard.",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
|
"stepVenue": "Venue",
|
||||||
|
"stepDevice": "Device",
|
||||||
|
"stepLinked": "Linked",
|
||||||
|
"noVenuesFound": "No venues found. Create one on the dashboard first.",
|
||||||
|
"changeVenue": "Change",
|
||||||
|
"deviceNameHint": "e.g. Bar terminal 1",
|
||||||
|
"registrationCodeHelper": "Find this code on the dashboard under devices.",
|
||||||
|
"venueNowLinked": "This device is now linked to {venue}.",
|
||||||
|
"@venueNowLinked": {"placeholders": {"venue": {"type": "String"}}},
|
||||||
"scanToPay": "Let the customer scan this code to pay {amount}",
|
"scanToPay": "Let the customer scan this code to pay {amount}",
|
||||||
"@scanToPay": {
|
"@scanToPay": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -1880,6 +1880,54 @@ abstract class AppLocalizations {
|
|||||||
/// **'Done'**
|
/// **'Done'**
|
||||||
String get done;
|
String get done;
|
||||||
|
|
||||||
|
/// No description provided for @stepVenue.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Venue'**
|
||||||
|
String get stepVenue;
|
||||||
|
|
||||||
|
/// No description provided for @stepDevice.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Device'**
|
||||||
|
String get stepDevice;
|
||||||
|
|
||||||
|
/// No description provided for @stepLinked.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Linked'**
|
||||||
|
String get stepLinked;
|
||||||
|
|
||||||
|
/// No description provided for @noVenuesFound.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No venues found. Create one on the dashboard first.'**
|
||||||
|
String get noVenuesFound;
|
||||||
|
|
||||||
|
/// No description provided for @changeVenue.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Change'**
|
||||||
|
String get changeVenue;
|
||||||
|
|
||||||
|
/// No description provided for @deviceNameHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'e.g. Bar terminal 1'**
|
||||||
|
String get deviceNameHint;
|
||||||
|
|
||||||
|
/// No description provided for @registrationCodeHelper.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Find this code on the dashboard under devices.'**
|
||||||
|
String get registrationCodeHelper;
|
||||||
|
|
||||||
|
/// No description provided for @venueNowLinked.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This device is now linked to {venue}.'**
|
||||||
|
String venueNowLinked(String venue);
|
||||||
|
|
||||||
/// No description provided for @scanToPay.
|
/// No description provided for @scanToPay.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -977,6 +977,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get done => 'Done';
|
String get done => 'Done';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepVenue => 'Venue';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepDevice => 'Device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepLinked => 'Linked';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noVenuesFound =>
|
||||||
|
'No venues found. Create one on the dashboard first.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get changeVenue => 'Change';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deviceNameHint => 'e.g. Bar terminal 1';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registrationCodeHelper =>
|
||||||
|
'Find this code on the dashboard under devices.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String venueNowLinked(String venue) {
|
||||||
|
return 'This device is now linked to $venue.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String scanToPay(String amount) {
|
String scanToPay(String amount) {
|
||||||
return 'Let the customer scan this code to pay $amount';
|
return 'Let the customer scan this code to pay $amount';
|
||||||
|
|||||||
@@ -990,6 +990,34 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get done => 'Klaar';
|
String get done => 'Klaar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepVenue => 'Locatie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepDevice => 'Toestel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get stepLinked => 'Gekoppeld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noVenuesFound =>
|
||||||
|
'Geen locaties gevonden. Maak er eerst een aan op het dashboard.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get changeVenue => 'Wijzigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deviceNameHint => 'bv. Bartoestel 1';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registrationCodeHelper =>
|
||||||
|
'Je vindt deze code op het dashboard bij toestellen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String venueNowLinked(String venue) {
|
||||||
|
return 'Dit toestel is nu gekoppeld aan $venue.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String scanToPay(String amount) {
|
String scanToPay(String amount) {
|
||||||
return 'Laat de klant deze code scannen om $amount te betalen';
|
return 'Laat de klant deze code scannen om $amount te betalen';
|
||||||
|
|||||||
@@ -318,5 +318,13 @@
|
|||||||
"deviceRegistered": "Toestel geregistreerd!",
|
"deviceRegistered": "Toestel geregistreerd!",
|
||||||
"openTabsWillSync": "Open poefs worden gesynchroniseerd met het dashboard.",
|
"openTabsWillSync": "Open poefs worden gesynchroniseerd met het dashboard.",
|
||||||
"done": "Klaar",
|
"done": "Klaar",
|
||||||
|
"stepVenue": "Locatie",
|
||||||
|
"stepDevice": "Toestel",
|
||||||
|
"stepLinked": "Gekoppeld",
|
||||||
|
"noVenuesFound": "Geen locaties gevonden. Maak er eerst een aan op het dashboard.",
|
||||||
|
"changeVenue": "Wijzigen",
|
||||||
|
"deviceNameHint": "bv. Bartoestel 1",
|
||||||
|
"registrationCodeHelper": "Je vindt deze code op het dashboard bij toestellen.",
|
||||||
|
"venueNowLinked": "Dit toestel is nu gekoppeld aan {venue}.",
|
||||||
"scanToPay": "Laat de klant deze code scannen om {amount} te betalen"
|
"scanToPay": "Laat de klant deze code scannen om {amount} te betalen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,19 @@ class RegistrationViewModel extends ChangeNotifier {
|
|||||||
|
|
||||||
void selectVenue(Venue venue) {
|
void selectVenue(Venue venue) {
|
||||||
_selectedVenue = venue;
|
_selectedVenue = venue;
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectVenue() {
|
||||||
|
_selectedVenue = null;
|
||||||
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchVenues() async {
|
Future<void> fetchVenues() async {
|
||||||
_step = RegistrationStep.loadingVenues;
|
_step = RegistrationStep.loadingVenues;
|
||||||
|
_selectedVenue = null;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -96,12 +104,4 @@ class RegistrationViewModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> retry() => fetchVenues();
|
Future<void> retry() => fetchVenues();
|
||||||
|
|
||||||
void reset() {
|
|
||||||
_step = RegistrationStep.initial;
|
|
||||||
_venues = [];
|
|
||||||
_selectedVenue = null;
|
|
||||||
_errorMessage = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class _PinEntryViewState extends State<PinEntryView> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
|
|||||||
+346
-223
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
@@ -6,6 +9,8 @@ import '../utils/navigation.dart';
|
|||||||
import '../viewmodels/registration_view_model.dart';
|
import '../viewmodels/registration_view_model.dart';
|
||||||
import '../viewmodels/remote_sync_view_model.dart';
|
import '../viewmodels/remote_sync_view_model.dart';
|
||||||
|
|
||||||
|
const _codeLength = 6;
|
||||||
|
|
||||||
class RegistrationView extends StatefulWidget {
|
class RegistrationView extends StatefulWidget {
|
||||||
const RegistrationView({super.key});
|
const RegistrationView({super.key});
|
||||||
|
|
||||||
@@ -26,8 +31,6 @@ class _RegistrationViewState extends State<RegistrationView> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLocalizations get _l10n => AppLocalizations.of(context);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_deviceNameController.dispose();
|
_deviceNameController.dispose();
|
||||||
@@ -35,6 +38,23 @@ class _RegistrationViewState extends State<RegistrationView> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _register(RegistrationViewModel vm) async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
|
await vm.registerDevice(
|
||||||
|
deviceName: _deviceNameController.text.trim(),
|
||||||
|
registrationCode: _regCodeController.text.trim().toUpperCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finish() {
|
||||||
|
// The redirect away from /register only sees the base location of a
|
||||||
|
// pushed route, so leave the screen ourselves.
|
||||||
|
unawaited(context.read<RemoteSyncViewModel>().activate());
|
||||||
|
context.popOrGo('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.watch<RegistrationViewModel>();
|
final vm = context.watch<RegistrationViewModel>();
|
||||||
@@ -44,11 +64,17 @@ class _RegistrationViewState extends State<RegistrationView> {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
leading: IconButton(
|
title: Text(l10n.connectToRemoteServer),
|
||||||
tooltip: l10n.cancel,
|
leading: vm.step == RegistrationStep.registered
|
||||||
onPressed: () => context.popOrGo('/admin'),
|
? null
|
||||||
icon: const Icon(Icons.close_rounded),
|
: IconButton(
|
||||||
),
|
tooltip: l10n.cancel,
|
||||||
|
onPressed: vm.step == RegistrationStep.registering
|
||||||
|
? null
|
||||||
|
: () => context.popOrGo('/admin'),
|
||||||
|
icon: const Icon(Icons.close_rounded),
|
||||||
|
),
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -56,7 +82,15 @@ class _RegistrationViewState extends State<RegistrationView> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: _buildStep(vm, l10n),
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_StepIndicator(current: _stepIndex(vm)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildStep(vm, l10n),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -64,333 +98,422 @@ class _RegistrationViewState extends State<RegistrationView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _stepIndex(RegistrationViewModel vm) {
|
||||||
|
if (vm.step == RegistrationStep.registered) return 2;
|
||||||
|
return vm.selectedVenue == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStep(RegistrationViewModel vm, AppLocalizations l10n) {
|
Widget _buildStep(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||||
switch (vm.step) {
|
switch (vm.step) {
|
||||||
|
case RegistrationStep.initial:
|
||||||
case RegistrationStep.loadingVenues:
|
case RegistrationStep.loadingVenues:
|
||||||
return _buildLoading();
|
return _buildLoading(l10n);
|
||||||
case RegistrationStep.error:
|
case RegistrationStep.error:
|
||||||
return _buildError(vm);
|
return _buildError(vm, l10n);
|
||||||
case RegistrationStep.venuesLoaded:
|
case RegistrationStep.venuesLoaded:
|
||||||
|
case RegistrationStep.registering:
|
||||||
if (vm.selectedVenue == null) {
|
if (vm.selectedVenue == null) {
|
||||||
return _buildVenueSelection(vm, l10n);
|
return _buildVenueSelection(vm, l10n);
|
||||||
}
|
}
|
||||||
return _buildDeviceDetails(vm, l10n);
|
return _buildDeviceDetails(vm, l10n);
|
||||||
case RegistrationStep.registering:
|
|
||||||
return _buildRegistering();
|
|
||||||
case RegistrationStep.registered:
|
case RegistrationStep.registered:
|
||||||
return _buildSuccess(vm, l10n);
|
return _buildSuccess(vm, l10n);
|
||||||
case RegistrationStep.initial:
|
|
||||||
return _buildLoading();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoading() {
|
Widget _buildLoading(AppLocalizations l10n) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 40),
|
||||||
Icon(
|
|
||||||
Icons.cloud_outlined,
|
|
||||||
size: 64,
|
|
||||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
const CircularProgressIndicator(strokeWidth: 3),
|
const CircularProgressIndicator(strokeWidth: 3),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
_l10n.connectingToServer,
|
l10n.connectingToServer,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildError(RegistrationViewModel vm) {
|
Widget _buildError(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 60),
|
Icon(Icons.cloud_off_rounded, size: 56, color: scheme.error),
|
||||||
Icon(
|
const SizedBox(height: 16),
|
||||||
Icons.cloud_off_rounded,
|
|
||||||
size: 64,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
Text(
|
||||||
_l10n.failedToConnect,
|
l10n.failedToConnect,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
vm.errorMessage ?? _l10n.unknownError,
|
vm.errorMessage ?? l10n.unknownError,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () => vm.retry(),
|
onPressed: vm.retry,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: Text(_l10n.retry),
|
label: Text(l10n.retry),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) {
|
Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 40),
|
|
||||||
Icon(
|
|
||||||
Icons.cloud_outlined,
|
|
||||||
size: 48,
|
|
||||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
Text(
|
||||||
_l10n.connectToRemoteServer,
|
l10n.selectVenue,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
_l10n.selectVenueToLink,
|
l10n.selectVenueToLink,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Text(
|
|
||||||
_l10n.selectVenue,
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.15),
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
for (final venue in vm.venues)
|
|
||||||
Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => vm.selectVenue(venue),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.store_outlined,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
venue.name,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.chevron_right_rounded,
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextButton(
|
if (vm.venues.isEmpty)
|
||||||
onPressed: () => vm.retry(),
|
Container(
|
||||||
child: Text(_l10n.refreshVenues),
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.storefront_outlined,
|
||||||
|
size: 40,
|
||||||
|
color: scheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
l10n.noVenuesFound,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final venue in vm.venues)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Material(
|
||||||
|
color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => vm.selectVenue(venue),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: scheme.primaryContainer,
|
||||||
|
foregroundColor: scheme.onPrimaryContainer,
|
||||||
|
child: const Icon(Icons.storefront_outlined),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
venue.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
color: scheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: vm.retry,
|
||||||
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
|
label: Text(l10n.refreshVenues),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDeviceDetails(RegistrationViewModel vm, AppLocalizations l10n) {
|
Widget _buildDeviceDetails(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final isBusy = vm.step == RegistrationStep.registering;
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 40),
|
Container(
|
||||||
Icon(
|
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
|
||||||
Icons.store_outlined,
|
decoration: BoxDecoration(
|
||||||
size: 48,
|
color: scheme.primaryContainer.withValues(alpha: 0.5),
|
||||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.storefront_outlined, color: scheme.primary),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
vm.selectedVenue!.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: isBusy ? null : vm.deselectVenue,
|
||||||
|
child: Text(l10n.changeVenue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
l10n.enterDeviceDetails,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
|
||||||
vm.selectedVenue!.name,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
_l10n.enterDeviceDetails,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _deviceNameController,
|
controller: _deviceNameController,
|
||||||
|
enabled: !isBusy,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: _l10n.deviceName,
|
labelText: l10n.deviceName,
|
||||||
hintText: 'e.g. Bar terminal 1',
|
hintText: l10n.deviceNameHint,
|
||||||
border: OutlineInputBorder(),
|
prefixIcon: const Icon(Icons.tablet_android_outlined),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
v == null || v.trim().isEmpty ? _l10n.deviceNameRequired : null,
|
v == null || v.trim().isEmpty ? l10n.deviceNameRequired : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _regCodeController,
|
controller: _regCodeController,
|
||||||
decoration: InputDecoration(
|
enabled: !isBusy,
|
||||||
labelText: _l10n.registrationCode,
|
autofocus: true,
|
||||||
hintText: '6-character code from dashboard',
|
maxLength: _codeLength,
|
||||||
border: OutlineInputBorder(),
|
textAlign: TextAlign.center,
|
||||||
counterText: '',
|
|
||||||
),
|
|
||||||
maxLength: 6,
|
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
validator: (v) => v == null || v.trim().length < 6
|
textInputAction: TextInputAction.done,
|
||||||
? _l10n.enterRegistrationCode
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp('[a-zA-Z0-9]')),
|
||||||
|
_UpperCaseFormatter(),
|
||||||
|
],
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
letterSpacing: 8,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.registrationCode,
|
||||||
|
hintText: '•' * _codeLength,
|
||||||
|
helperText: l10n.registrationCodeHelper,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onFieldSubmitted: (_) => _register(vm),
|
||||||
|
validator: (v) => v == null || v.trim().length < _codeLength
|
||||||
|
? l10n.enterRegistrationCode
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
if (vm.errorMessage != null) ...[
|
||||||
if (vm.errorMessage != null)
|
const SizedBox(height: 8),
|
||||||
Padding(
|
Container(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
vm.errorMessage!,
|
color: scheme.errorContainer,
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: Theme.of(context).colorScheme.error,
|
),
|
||||||
fontSize: 13,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: scheme.onErrorContainer),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
vm.errorMessage!,
|
||||||
|
style: TextStyle(color: scheme.onErrorContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
Row(
|
const SizedBox(height: 24),
|
||||||
children: [
|
FilledButton(
|
||||||
TextButton(
|
style: FilledButton.styleFrom(
|
||||||
onPressed: () {
|
minimumSize: const Size.fromHeight(52),
|
||||||
vm.reset();
|
),
|
||||||
},
|
onPressed: isBusy ? null : () => _register(vm),
|
||||||
child: Text(_l10n.back),
|
child: isBusy
|
||||||
),
|
? Row(
|
||||||
const SizedBox(width: 12),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Expanded(
|
children: [
|
||||||
child: FilledButton(
|
const SizedBox(
|
||||||
onPressed: () async {
|
width: 18,
|
||||||
if (!_formKey.currentState!.validate()) return;
|
height: 18,
|
||||||
await vm.registerDevice(
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
deviceName: _deviceNameController.text.trim(),
|
),
|
||||||
registrationCode: _regCodeController.text
|
const SizedBox(width: 12),
|
||||||
.trim()
|
Text(l10n.registeringDevice),
|
||||||
.toUpperCase(),
|
],
|
||||||
);
|
)
|
||||||
},
|
: Text(l10n.register),
|
||||||
child: Text(_l10n.register),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRegistering() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 60),
|
|
||||||
const CircularProgressIndicator(strokeWidth: 3),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
_l10n.registeringDevice,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSuccess(RegistrationViewModel vm, AppLocalizations l10n) {
|
Widget _buildSuccess(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 40),
|
Center(
|
||||||
Icon(
|
child: Container(
|
||||||
Icons.check_circle_rounded,
|
padding: const EdgeInsets.all(20),
|
||||||
size: 80,
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: scheme.primaryContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_rounded,
|
||||||
|
size: 56,
|
||||||
|
color: scheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
_l10n.deviceRegistered,
|
l10n.deviceRegistered,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'${vm.selectedVenue?.name ?? "Venue"} is now linked.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_l10n.openTabsWillSync,
|
l10n.venueNowLinked(vm.selectedVenue?.name ?? ''),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
color: Theme.of(
|
),
|
||||||
context,
|
const SizedBox(height: 4),
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
Text(
|
||||||
|
l10n.openTabsWillSync,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
// Once configured, the router redirects to /bar by itself.
|
style: FilledButton.styleFrom(
|
||||||
onPressed: () => context.read<RemoteSyncViewModel>().activate(),
|
minimumSize: const Size.fromHeight(52),
|
||||||
|
),
|
||||||
|
onPressed: _finish,
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
label: Text(_l10n.done),
|
label: Text(l10n.done),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StepIndicator extends StatelessWidget {
|
||||||
|
final int current;
|
||||||
|
|
||||||
|
const _StepIndicator({required this.current});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final labels = [l10n.stepVenue, l10n.stepDevice, l10n.stepLinked];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < labels.length; i++) ...[
|
||||||
|
if (i > 0)
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 2,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
color: i <= current
|
||||||
|
? scheme.primary
|
||||||
|
: scheme.onSurface.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: i <= current
|
||||||
|
? scheme.primary
|
||||||
|
: scheme.onSurface.withValues(alpha: 0.1),
|
||||||
|
foregroundColor: i <= current
|
||||||
|
? scheme.onPrimary
|
||||||
|
: scheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
child: i < current
|
||||||
|
? const Icon(Icons.check_rounded, size: 16)
|
||||||
|
: Text(
|
||||||
|
'${i + 1}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
labels[i],
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: i <= current
|
||||||
|
? scheme.onSurface
|
||||||
|
: scheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpperCaseFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
return newValue.copyWith(text: newValue.text.toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: kooltap
|
|||||||
description: "KoolTap: a tablet point-of-sale app for running bar tabs."
|
description: "KoolTap: a tablet point-of-sale app for running bar tabs."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.3.8+0
|
version: 1.3.9+0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
Reference in New Issue
Block a user