blob: 92a74ac4b4880594db36d7c2c49fd4c75a118cef [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ermine/src/states/app_state.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:internationalization/strings.dart';
const _kPageVerticalPaddings = 100.0;
const _kContentWidth = 744.0;
/// Full-screen pages to handle the user feedback flow.
class UserFeedback extends StatelessWidget {
final AppState app;
const UserFeedback(this.app);
@override
Widget build(BuildContext context) => Observer(builder: (context) {
final isScrim = app.feedbackPage == FeedbackPage.scrim;
// TODO(fxb/88445): Implement other pages
switch (app.feedbackPage) {
case FeedbackPage.scrim:
case FeedbackPage.ready:
return Stack(
children: [
UserFeedbackForm(app, isOutFocused: isScrim),
if (app.feedbackPage == FeedbackPage.scrim)
Container(
color: Theme.of(context).canvasColor.withOpacity(0.6),
),
],
);
case FeedbackPage.submitted:
return UserFeedbackSubmitted(app);
case FeedbackPage.failed:
return UserFeedbackError(app);
default:
return Offstage();
}
});
}
/// A page that displays a form to get the user data to file their feedback
class UserFeedbackForm extends StatelessWidget {
final AppState app;
final bool isOutFocused;
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descController = TextEditingController();
final _usernameController = TextEditingController();
final _descFocusNode = FocusNode();
UserFeedbackForm(this.app, {required this.isOutFocused});
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = app.simpleLocale;
final _legalHelpUrl = _lang.isEmpty
? 'https://policies.google.com/terms'
: 'https://policies.google.com/terms?hl=$_lang';
final _privacyPolicyUrl = _lang.isEmpty
? 'https://policies.google.com/privacy'
: 'https://policies.google.com/privacy?hl=$_lang';
final _termsOfServiceUrl = _lang.isEmpty
? 'https://policies.google.com/terms'
: 'https://policies.google.com/terms?hl=$_lang';
return FocusScope(
child: Container(
padding: EdgeInsets.symmetric(vertical: _kPageVerticalPaddings),
color: _theme.bottomAppBarColor,
child: Center(
child: SizedBox(
width: _kContentWidth,
child: Observer(builder: (context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
Strings.sendFeedback,
style: Theme.of(context).textTheme.headline5,
),
SizedBox(height: 40),
]),
// Scrollable body
Expanded(
child: SingleChildScrollView(
child: SizedBox(
width: _kContentWidth,
child: Form(
key: _formKey,
child: Column(
children: [
// IssueTitle
TextFormField(
maxLines: 1,
controller: _titleController,
autofocus: !isOutFocused,
decoration: InputDecoration(
labelText: Strings.issueTitle,
border: OutlineInputBorder(
borderRadius: BorderRadius.zero),
),
),
SizedBox(height: 36),
// Description
TextFormField(
minLines: 5,
maxLines: 5,
textAlignVertical: TextAlignVertical.top,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
controller: _descController,
focusNode: _descFocusNode,
autovalidateMode:
AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
labelText: '${Strings.description}*',
hintText: Strings.noPII,
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.zero),
),
// TODO(fxb/79807): Remove this workaround once the bug is fixed.
onFieldSubmitted: (desc) {
final cursorPos =
_descController.selection.base.offset;
final textBeforeCursor = _descController
.value.selection
.textBefore(desc);
final textAfterCursor = _descController
.value.selection
.textAfter(desc);
_descController
..text =
'$textBeforeCursor\n$textAfterCursor'
..selection = TextSelection.fromPosition(
TextPosition(offset: cursorPos + 1));
FocusScope.of(context)
.requestFocus(_descFocusNode);
},
validator: (value) =>
value == null || value.isEmpty
? Strings.needDescription
: null,
),
SizedBox(height: 36),
// Username
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: TextFormField(
maxLines: 1,
controller: _usernameController,
autovalidateMode:
AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
labelText: '${Strings.username}*',
border: OutlineInputBorder(
borderRadius: BorderRadius.zero),
),
validator: (value) =>
value == null || value.isEmpty
? Strings.needUsername
: null,
)),
SizedBox(width: 16),
Text('@google.com',
style: _theme.textTheme.bodyText1),
],
),
// TODO(fxb/97464): Add the screenshot UX when the bug is fixed.
SizedBox(height: 40),
MarkdownRichText(
Strings.dataSharingLegalStatement(_legalHelpUrl,
_privacyPolicyUrl, _termsOfServiceUrl),
urlLauncher: (url) {
app.launch(url, url);
},
),
],
),
),
),
),
),
SizedBox(height: 40),
// Footer (CTAs)
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
OutlinedButton(
child: Text(Strings.cancel.toUpperCase()),
onPressed: app.closeUserFeedback,
),
SizedBox(width: 16),
ElevatedButton(
child: Text(Strings.submit.toUpperCase()),
onPressed: () =>
_formKey.currentState?.validate() ?? false
? app.userFeedbackSubmit(
title: _titleController.text,
desc: _descController.text,
username: _usernameController.text,
)
: null,
),
],
),
],
);
}),
),
),
),
);
}
}
/// A page displayed when the feedback report submission is completed.
class UserFeedbackSubmitted extends StatelessWidget {
final AppState app;
const UserFeedbackSubmitted(this.app);
@override
Widget build(BuildContext context) => Container(
padding: EdgeInsets.symmetric(vertical: _kPageVerticalPaddings),
color: Theme.of(context).bottomAppBarColor,
child: FocusScope(
child: Center(
child: SizedBox(
width: _kContentWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(Strings.submittedTitle,
style: Theme.of(context).textTheme.headline5),
SizedBox(height: 32),
Text(Strings.submittedDesc(app.feedbackUuid),
style: Theme.of(context).textTheme.bodyText1),
],
),
),
OutlinedButton(
child: Text(Strings.close.toUpperCase()),
style: ErmineButtonStyle.outlinedButton(Theme.of(context)),
onPressed: app.closeUserFeedback,
),
],
),
),
),
),
);
}
/// A page displayed when filing report has been failed.
class UserFeedbackError extends StatelessWidget {
final AppState app;
const UserFeedbackError(this.app);
@override
Widget build(BuildContext context) => Container(
padding: EdgeInsets.symmetric(vertical: _kPageVerticalPaddings),
color: Theme.of(context).bottomAppBarColor,
child: FocusScope(
child: Center(
child: SizedBox(
width: _kContentWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(Strings.failedToFileTitle,
style: Theme.of(context).textTheme.headline5),
SizedBox(height: 32),
Text(app.feedbackErrorMsg,
style: Theme.of(context).textTheme.bodyText1),
SizedBox(height: 24),
Text(
Strings.failedToFileDesc('go/workstation-feedback'),
style: Theme.of(context).textTheme.bodyText1),
],
),
),
OutlinedButton(
child: Text(Strings.close.toUpperCase()),
style: ErmineButtonStyle.outlinedButton(Theme.of(context)),
onPressed: app.closeUserFeedback,
),
],
),
),
),
),
);
}