Skip to content

Commit

Permalink
feat: further improve user experience and clarify auth procedures
Browse files Browse the repository at this point in the history
  • Loading branch information
WesselvanDam committed Dec 30, 2024
1 parent 613fb24 commit d8c04de
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 123 deletions.
12 changes: 7 additions & 5 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import 'package:thank_you_token/providers/user_provider.dart';
import 'package:thank_you_token/services/drive/drive_service.dart';
import 'package:thank_you_token/utils/extensions.dart';
import 'package:url_strategy/url_strategy.dart';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:thank_you_token/services/auth/auth_service.dart';
import 'package:thank_you_token/services/router/router.dart';

late final bool initialLogIn;

void main() async {
setPathUrlStrategy();
initialLogIn = await googleSignIn.signInSilently() != null;
runApp(const ProviderScope(child: MyApp()));
final signedInUser = await DriveServiceApi().signInSilently();
runApp(ProviderScope(
overrides: [initialUserProvider.overrideWithValue(signedInUser)],
child: const MyApp(),
));
}

class MyApp extends ConsumerWidget {
Expand Down
34 changes: 22 additions & 12 deletions lib/providers/user_provider.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:thank_you_token/services/auth/auth_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:thank_you_token/services/drive/drive_service.dart';

// If there is an initial login, the user is googleSignIn.currentUser. Return
// that, but afterwards listen to googleSignIn.onCurrentUserChanged and return
// the new user.
final userProvider = StreamProvider<GoogleSignInAccount?>(
(ref) => StreamGroup.merge([
Stream.value(googleSignIn.currentUser),
googleSignIn.onCurrentUserChanged
]),
);
part 'user_provider.g.dart';

@Riverpod(keepAlive: true)
class User extends _$User {
@override
GoogleSignInAccount? build() {
final initialUser = ref.watch(initialUserProvider);
if (initialUser != null) {
return initialUser;
} else {
DriveServiceApi().onCurrentUserChanged().listen((user) {
state = user;
});
return null;
}
}
}

@Riverpod(keepAlive: true)
GoogleSignInAccount? initialUser(InitialUserRef ref) => null;
25 changes: 17 additions & 8 deletions lib/screens/Details/details_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@ import 'package:thank_you_token/models/token.dart';
import 'package:thank_you_token/providers/token_provider.dart';
import 'package:thank_you_token/screens/Details/edit_provider.dart';
import 'package:thank_you_token/screens/Details/local/details_info.dart';
import 'package:thank_you_token/utils/authorisation_check.dart';
import 'package:thank_you_token/utils/extensions.dart';
import 'package:thank_you_token/widgets/token_image.dart';

class DetailsScreen extends ConsumerWidget {
const DetailsScreen({super.key});

void _handleEdit(WidgetRef ref, Token token) {
Future<void> _handleEdit(
BuildContext context, WidgetRef ref, Token token) async {
if (!(await checkAuthorisation(context) ?? false)) return;

ref.read(tokenEditProvider.notifier).setToken(token);
}

void _handleSave(WidgetRef ref, Token token) {
Future<void> _handleSave(
BuildContext context, WidgetRef ref, Token token) async {
if (ref.read(tokenEditProvider) != token) {
if (!(await checkAuthorisation(context) ?? false)) return;
ref
.read(tokensProvider.notifier)
.updateToken(ref.read(tokenEditProvider)!);
Expand All @@ -43,12 +49,13 @@ class DetailsScreen extends ConsumerWidget {
],
),
).then(
(shouldDelete) {
(shouldDelete) async {
if (!shouldDelete) return;
if (!(await checkAuthorisation(context) ?? false)) return;

ref.read(tokensProvider.notifier).deleteToken(token);
context.pop();
},
);
).then((_) => context.pop());
}

@override
Expand All @@ -75,17 +82,19 @@ class DetailsScreen extends ConsumerWidget {
aspectRatio: 16 / 9,
child: TokenImage(token, borderRadius: 24),
),
const SizedBox(height: 12),
Column(
children: [
DetailsInfo(token.fromInfo),
const Divider(indent: 24, endIndent: 24),
DetailsInfo(token.toInfo),
],
),
const SizedBox(height: 12),
ButtonBar(
alignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
TextButton.icon(
icon: const Icon(Icons.delete),
label: const Text('Delete'),
onPressed: () => _handleDelete(context, ref, token),
Expand All @@ -94,13 +103,13 @@ class DetailsScreen extends ConsumerWidget {
FilledButton.icon(
icon: const Icon(Icons.save),
label: const Text('Save'),
onPressed: () => _handleSave(ref, token),
onPressed: () => _handleSave(context, ref, token),
)
else
FilledButton.tonalIcon(
icon: const Icon(Icons.edit),
label: const Text('Edit'),
onPressed: () => _handleEdit(ref, token),
onPressed: () => _handleEdit(context, ref, token),
),
],
),
Expand Down
28 changes: 23 additions & 5 deletions lib/screens/Details/local/details_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,36 @@ import 'package:thank_you_token/screens/Details/edit_provider.dart';
import 'package:thank_you_token/widgets/details_icon.dart';
import 'package:thank_you_token/widgets/details_name.dart';

class DetailsInfo extends ConsumerWidget {
class DetailsInfo extends ConsumerStatefulWidget {
const DetailsInfo(this.info, {super.key});

final TokenPartialInfo info;

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<ConsumerStatefulWidget> createState() => _DetailsInfoState();
}

class _DetailsInfoState extends ConsumerState<DetailsInfo> {

late TextEditingController _nameController;
late TextEditingController _descriptionController;

@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.info.name);
_descriptionController = TextEditingController(text: widget.info.message);
}

@override
Widget build(BuildContext context) {
return ref.watch(tokenEditProvider.select((value) => value == null))
? showInfo(context, ref)
: editInfo(context, ref);
}

Widget showInfo(BuildContext context, WidgetRef ref) {
final info = widget.info;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
Expand All @@ -36,7 +53,7 @@ class DetailsInfo extends ConsumerWidget {
),
),
Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
info.message ?? 'No description added',
style: Theme.of(context).textTheme.bodyMedium,
Expand All @@ -47,6 +64,7 @@ class DetailsInfo extends ConsumerWidget {
}

Widget editInfo(BuildContext context, WidgetRef ref) {
final info = widget.info;
return FocusTraversalGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
Expand All @@ -58,7 +76,7 @@ class DetailsInfo extends ConsumerWidget {
borderRadius: BorderRadius.circular(8),
child: TextField(
inputFormatters: [LengthLimitingTextInputFormatter(50)],
controller: TextEditingController(text: info.name),
controller: _nameController,
onChanged: (value) {
ref.read(tokenEditProvider.notifier).updateInfo(
info.isFrom,
Expand Down Expand Up @@ -121,7 +139,7 @@ class DetailsInfo extends ConsumerWidget {
minLines: 1,
maxLines: 8,
inputFormatters: [LengthLimitingTextInputFormatter(10000)],
controller: TextEditingController(text: info.message),
controller: _descriptionController,
onChanged: (value) {
ref.read(tokenEditProvider.notifier).updateInfo(
info.isFrom,
Expand Down
20 changes: 14 additions & 6 deletions lib/screens/Home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import 'package:smooth_scroll_multiplatform/smooth_scroll_multiplatform.dart';
import 'package:thank_you_token/providers/token_provider.dart';
import 'package:thank_you_token/providers/user_provider.dart';
import 'package:thank_you_token/screens/Home/local/token_grid.dart';
import 'package:thank_you_token/services/auth/auth_service.dart';
import 'package:thank_you_token/services/drive/drive_service.dart';
import 'package:url_launcher/url_launcher.dart';

class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider).value;
final user = ref.watch(userProvider);
if (user == null) return const SizedBox.shrink();

return DynMouseScroll(
builder: (context, controller, physics) => CustomScrollView(
Expand All @@ -28,7 +29,7 @@ class HomeScreen extends ConsumerWidget {
),
),
title: Text(
'Welcome, ${user?.displayName?.split(' ').first ?? 'Guest'}',
'Welcome, ${user.displayName?.split(' ').first ?? 'Guest'}',
),
actions: actions(ref),
),
Expand All @@ -39,6 +40,13 @@ class HomeScreen extends ConsumerWidget {
}

List<Widget> actions(WidgetRef ref) => [
if (ref.watch(userProvider)?.photoUrl != null)
Tooltip(
message: 'Signed in as ${ref.watch(userProvider)!.email}',
child: CircleAvatar(
backgroundImage: NetworkImage(ref.watch(userProvider)!.photoUrl!),
),
),
PopupMenuButton<String>(
popUpAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
Expand All @@ -49,17 +57,17 @@ class HomeScreen extends ConsumerWidget {
),
onSelected: (value) {
switch (value) {
case 'Refresh':
case 'Refresh Tokens':
ref.invalidate(tokensProvider);
case 'About':
launchUrl(Uri.parse('https://thank-you-token.nl'));
case 'Sign out':
googleSignIn.disconnect();
DriveServiceApi().signOut();
}
},
itemBuilder: (BuildContext context) {
return {
('Refresh', Icons.refresh),
('Refresh Tokens', Icons.refresh),
('About', Icons.info),
('Sign out', Icons.logout),
}.map((choice) {
Expand Down
95 changes: 57 additions & 38 deletions lib/screens/Home/local/first_token_prompt.dart
Original file line number Diff line number Diff line change
@@ -1,54 +1,73 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:thank_you_token/utils/picker.dart';
import 'package:thank_you_token/utils/image_picker.dart';

class FirstTokenPrompt extends ConsumerWidget {
class FirstTokenPrompt extends ConsumerStatefulWidget {
const FirstTokenPrompt({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<ConsumerStatefulWidget> createState() =>
_FirstTokenPromptState();
}

class _FirstTokenPromptState extends ConsumerState<FirstTokenPrompt> {
bool isAsync = false;

void _handleAddToken() {
if (isAsync) return;
setState(() => isAsync = true);
addTokenSequence(context, ref).then((_) => setState(() => isAsync = false));
}

@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Image.asset(
'assets/images/store_illustration.png',
height: 810 * (400 / 1080),
width: 400,
),
const SizedBox(height: 24),
Text.rich(
textAlign: TextAlign.center,
TextSpan(
text: "Create your first token!",
style: Theme.of(context).textTheme.headlineSmall,
children: [
TextSpan(
text:
'\n\nTake a picture of your token and add it to your collection.',
style: Theme.of(context).textTheme.labelLarge,
),
TextSpan(
text: ' Read more.',
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => showMoreInfo(context),
),
],
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/store_illustration.png',
height: 810 * (400 / 1080),
width: 400,
),
),
const SizedBox(height: 12),
FilledButton.icon(
icon: const Icon(Icons.add),
onPressed: () => addTokenSequence(context, ref),
label: const Text('Add token'),
),
]),
const SizedBox(height: 24),
Text.rich(
textAlign: TextAlign.center,
TextSpan(
text: "Create your first token!",
style: Theme.of(context).textTheme.headlineSmall,
children: [
TextSpan(
text:
'\n\nFirst, take a picture of your token. Next, tap the button below to add it to your collection.',
style: Theme.of(context).textTheme.labelLarge,
),
TextSpan(
text: ' Read more.',
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => showMoreInfo(context),
),
],
),
),
const SizedBox(height: 12),
isAsync
? const CircularProgressIndicator()
: FilledButton.icon(
icon: const Icon(Icons.add),
onPressed: () => _handleAddToken(),
label: const Text('Add token'),
),
],
),
),
),
);
Expand Down
Loading

0 comments on commit d8c04de

Please sign in to comment.