Skip to content

Commit

Permalink
feat: account settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Jan Czizikow committed Apr 11, 2020
1 parent 456e542 commit 2aeb182
Show file tree
Hide file tree
Showing 22 changed files with 1,235 additions and 58 deletions.
7 changes: 7 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
4 changes: 4 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) would like access to your photo gallery</string>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) would like to use your camera</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:sentry/sentry.dart';

import './providers/auth.dart';
import './providers/account.dart';
import './providers/theme.dart';
import './providers/groups.dart';
import './screens/root.dart';
Expand All @@ -16,6 +17,8 @@ import './screens/forgot_password.dart';
import './screens/register.dart';
import './screens/group.dart';
import './screens/settings.dart';
import './screens/edit_name.dart';
import './screens/edit_email.dart';

Future<Null> main() async {
await DotEnv().load('.env');
Expand Down Expand Up @@ -77,13 +80,18 @@ class _MyAppState extends State<MyApp> {
GroupScreen.routeName: (context) =>
GroupScreen(arguments: ModalRoute.of(context).settings.arguments),
SettingsScreen.routeName: (context) => SettingsScreen(),
EditNameScreen.routeName: (context) => EditNameScreen(),
EditEmailScreen.routeName: (context) => EditEmailScreen(),
};

return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Auth(),
),
ChangeNotifierProvider(
create: (_) => AccountProvider(),
),
ChangeNotifierProvider(
create: (_) => themeProvider,
),
Expand Down
39 changes: 39 additions & 0 deletions lib/models/account.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/foundation.dart';

class Account {
String id;
String firstName;
String lastName;
String email;
String avatar;
final DateTime createdAt;
DateTime updatedAt;

Account({
@required this.id,
@required this.firstName,
@required this.lastName,
@required this.email,
@required this.createdAt,
this.avatar,
this.updatedAt,
});

get fullName => lastName.isNotEmpty ? "$firstName $lastName" : firstName;

get initials => RegExp(r'\S+').allMatches(fullName).fold('',
(acc, match) => acc + fullName.substring(match.start, match.start + 1));

factory Account.fromJson(Map<String, dynamic> json) {
return Account(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
email: json['email'],
avatar: json['avatar'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt:
json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
);
}
}
3 changes: 2 additions & 1 deletion lib/models/group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class Group {
currency: json['currency'],
isDeleted: json['isDeleted'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
updatedAt:
json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
);
}

Expand Down
70 changes: 70 additions & 0 deletions lib/providers/account.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:flutter/foundation.dart';

import '../models/account.dart';
import '../services/api.dart';

enum Status { IDLE, UPLOAD, ERROR }

class AccountProvider with ChangeNotifier {
Account _account;
Status _avatarStatus = Status.IDLE;
final Api _api = Api();

Account get account => _account;

String get fullName => account?.fullName ?? '';

String get initials => account?.initials ?? '';

bool get hasAvatar => account?.avatar != null;

Status get avatarStatus => _avatarStatus;

Future<void> fetchAccount() async {
Account account = await _api.fetchAccount();
_account = account;
notifyListeners();
}

Future<void> updateAccount(
{String email, String firstName, String lastName}) async {
Account account = await _api.updateAccount(
email: email ?? _account.email,
firstName: firstName ?? _account.firstName,
lastName: lastName ?? _account.lastName,
);
_account = account;
notifyListeners();
}

Future<void> uploadAvatar(String path) async {
_avatarStatus = Status.UPLOAD;
notifyListeners();

try {
final response = await _api.uploadAvatar(path);
if (response['status']) {
_account.avatar = response['data']['url'];
_avatarStatus = Status.IDLE;
} else {
_avatarStatus = Status.ERROR;
}
} catch (e) {
_avatarStatus = Status.ERROR;
} finally {
notifyListeners();
}
}

Future<void> removeAvatar() async {
await _api.removeAvatar();
_account.avatar = null;
notifyListeners();
}

Future<void> deleteAccount() async {
await _api.deleteAccount();
_account = null;
notifyListeners();
}
}
111 changes: 111 additions & 0 deletions lib/screens/edit_email.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../services/api.dart';
import '../providers/account.dart';
import '../widgets/platform_appbar.dart';
import '../widgets/platform_scaffold.dart';
import '../widgets/platform_alert_dialog.dart';
import '../widgets/platform_text_field.dart';

class EditEmailScreen extends StatefulWidget {
static const routeName = '/edit-email';

@override
_EditEmailState createState() => _EditEmailState();
}

class _EditEmailState extends State<EditEmailScreen> {
final _emailController = TextEditingController();
var _isLoading = false;

@override
void initState() {
super.initState();
final email =
Provider.of<AccountProvider>(context, listen: false).account.email;
_emailController.value = _emailController.value.copyWith(
text: email,
selection: TextSelection(
baseOffset: email.length,
extentOffset: email.length,
),
composing: TextRange.empty,
);
}

@override
void dispose() {
_emailController.dispose();
super.dispose();
}

void _showErrorMessage(String message) async {
showPlatformDialog(
context: context,
builder: (_) => PlatformAlertDialog(
title: Text('Error'),
content: Text(message),
),
);
}

void _editName() async {
setState(() => {_isLoading = true});
// TODO: validaton
final email = _emailController.text;

try {
await Provider.of<AccountProvider>(context, listen: false).updateAccount(
email: email,
);
Navigator.of(context).pop();
} on ApiError catch (err) {
_showErrorMessage(err.message);
} catch (err) {
_showErrorMessage(
'Failed to update, please check your internet connection and try again.');
} finally {
setState(() => {_isLoading = false});
}
}

@override
Widget build(BuildContext context) {
return PlatformScaffold(
appBar: PlatformAppBar(
title: Text('Edit Email'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: _isLoading ? null : _editName,
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
PlatformTextField(
autofocus: true,
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
),
onSubmitted: (_) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
_editName();
},
),
],
),
),
),
);
}
}
Loading

0 comments on commit 2aeb182

Please sign in to comment.