This repository has been archived by the owner on Feb 4, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #260 from OpenbookOrg/feature/250-invite-friends
Feature/250 invite friends
- Loading branch information
Showing
22 changed files
with
1,798 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import 'package:Openbook/models/updatable_model.dart'; | ||
import 'package:Openbook/models/user.dart'; | ||
import 'package:dcache/dcache.dart'; | ||
|
||
class UserInvite extends UpdatableModel<UserInvite> { | ||
final int id; | ||
String email; | ||
final DateTime created; | ||
User createdUser; | ||
String nickname; | ||
final String token; | ||
bool isInviteEmailSent; | ||
|
||
static getShareMessageForInviteWithToken(String token, String apiURL) { | ||
const String IOS_DOWNLOAD_LINK = 'https://testflight.apple.com/join/XniAjdyF'; | ||
const String ANDROID_DOWNLOAD_LINK = 'https://play.google.com/apps/testing/social.openbook.app'; | ||
String inviteLink = '$apiURL/api/auth/invite?token=$token'; | ||
|
||
String message = 'Hey, I\'d like to invite you to Openbook. First, Download the app on iTunes ($IOS_DOWNLOAD_LINK) or the Play store ($ANDROID_DOWNLOAD_LINK). ' | ||
'Second, paste this personalised invite link in the \'Sign up\' form in the Openbook App: $inviteLink '; | ||
|
||
return message; | ||
} | ||
|
||
static convertUserInviteStatusToBool(UserInviteFilterByStatus value) { | ||
bool isPending; | ||
switch (value) { | ||
case UserInviteFilterByStatus.all: | ||
isPending = null; | ||
break; | ||
case UserInviteFilterByStatus.pending: | ||
isPending = true; | ||
break; | ||
case UserInviteFilterByStatus.accepted: | ||
isPending = false; | ||
break; | ||
default: | ||
throw 'Unsupported post comment sort type'; | ||
} | ||
return isPending; | ||
} | ||
|
||
static void clearCache() { | ||
factory.clearCache(); | ||
} | ||
|
||
static final factory = UserInviteFactory(); | ||
|
||
factory UserInvite.fromJSON(Map<String, dynamic> json) { | ||
return factory.fromJson(json); | ||
} | ||
|
||
UserInvite({this.id, this.email, this.created, this.createdUser, this.nickname, this.token, this.isInviteEmailSent}); | ||
|
||
@override | ||
void updateFromJson(Map json) { | ||
if (json.containsKey('email')) { | ||
email = json['email']; | ||
} | ||
|
||
if (json.containsKey('created_user')) { | ||
createdUser = factory.parseUser(json['created_user']); | ||
} | ||
|
||
if (json.containsKey('nickname')) { | ||
nickname = json['nickname']; | ||
} | ||
|
||
if (json.containsKey('is_invite_email_sent')) { | ||
isInviteEmailSent = json['is_invite_email_sent']; | ||
} | ||
} | ||
} | ||
|
||
class UserInviteFactory extends UpdatableModelFactory<UserInvite> { | ||
@override | ||
SimpleCache<int, UserInvite> cache = | ||
LruCache(storage: UpdatableModelSimpleStorage(size: 10)); | ||
|
||
@override | ||
UserInvite makeFromJson(Map json) { | ||
DateTime created; | ||
var createdData = json['created']; | ||
if (createdData != null) created = DateTime.parse(createdData).toLocal(); | ||
|
||
return UserInvite( | ||
id: json['id'], | ||
email: json['email'], | ||
created: created, | ||
createdUser: parseUser(json['created_user']), | ||
nickname: json['nickname'], | ||
token: json['token'], | ||
isInviteEmailSent: json['is_invite_email_sent']); | ||
} | ||
|
||
User parseUser(Map userData) { | ||
if (userData == null) return null; | ||
return User.fromJson(userData); | ||
} | ||
} | ||
|
||
enum UserInviteFilterByStatus { pending, accepted, all } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import 'package:Openbook/models/user_invite.dart'; | ||
|
||
class UserInvitesList { | ||
final List<UserInvite> invites; | ||
|
||
UserInvitesList({ | ||
this.invites, | ||
}); | ||
|
||
factory UserInvitesList.fromJson(List<dynamic> parsedJson) { | ||
List<UserInvite> userInvites = | ||
parsedJson.map((inviteJson) => UserInvite.fromJSON(inviteJson)).toList(); | ||
|
||
return new UserInvitesList( | ||
invites: userInvites, | ||
); | ||
} | ||
} |
208 changes: 208 additions & 0 deletions
208
lib/pages/home/modals/user_invites/create_user_invite.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import 'package:Openbook/models/user_invite.dart'; | ||
import 'package:Openbook/services/navigation_service.dart'; | ||
import 'package:Openbook/widgets/icon.dart'; | ||
import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; | ||
import 'package:Openbook/provider.dart'; | ||
import 'package:Openbook/services/httpie.dart'; | ||
import 'package:Openbook/services/toast.dart'; | ||
import 'package:Openbook/services/user.dart'; | ||
import 'package:Openbook/services/validation.dart'; | ||
import 'package:Openbook/widgets/buttons/button.dart'; | ||
import 'package:Openbook/widgets/fields/text_form_field.dart'; | ||
import 'package:Openbook/widgets/page_scaffold.dart'; | ||
import 'package:Openbook/widgets/theming/primary_color_container.dart'; | ||
import 'package:async/async.dart'; | ||
import 'package:flutter/cupertino.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
class OBCreateUserInviteModal extends StatefulWidget { | ||
final UserInvite userInvite; | ||
final bool autofocusNameTextField; | ||
|
||
OBCreateUserInviteModal( | ||
{this.userInvite, this.autofocusNameTextField = false}); | ||
|
||
@override | ||
OBCreateUserInviteModalState createState() { | ||
return OBCreateUserInviteModalState(); | ||
} | ||
} | ||
|
||
class OBCreateUserInviteModalState | ||
extends State<OBCreateUserInviteModal> { | ||
|
||
UserService _userService; | ||
ToastService _toastService; | ||
NavigationService _navigationService; | ||
ValidationService _validationService; | ||
|
||
bool _requestInProgress; | ||
bool _formWasSubmitted; | ||
bool _formValid; | ||
bool _hasExistingUserInvite; | ||
|
||
GlobalKey<FormState> _formKey; | ||
|
||
TextEditingController _nicknameController; | ||
|
||
CancelableOperation _createUpdateOperation; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_formValid = true; | ||
_requestInProgress = false; | ||
_formWasSubmitted = false; | ||
_nicknameController = TextEditingController(); | ||
_formKey = GlobalKey<FormState>(); | ||
_hasExistingUserInvite = widget.userInvite != null; | ||
if (_hasExistingUserInvite) { | ||
_nicknameController.text = widget.userInvite.nickname; | ||
} | ||
|
||
_nicknameController.addListener(_updateFormValid); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
super.dispose(); | ||
if (_createUpdateOperation != null) | ||
_createUpdateOperation.cancel(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
var openbookProvider = OpenbookProvider.of(context); | ||
_userService = openbookProvider.userService; | ||
_toastService = openbookProvider.toastService; | ||
_validationService = openbookProvider.validationService; | ||
_navigationService = openbookProvider.navigationService; | ||
|
||
return OBCupertinoPageScaffold( | ||
navigationBar: _buildNavigationBar(), | ||
child: OBPrimaryColorContainer( | ||
child: SingleChildScrollView( | ||
physics: AlwaysScrollableScrollPhysics(), | ||
child: Form( | ||
key: _formKey, | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: <Widget>[ | ||
Padding( | ||
padding: EdgeInsets.symmetric(horizontal: 20.0), | ||
child: Column( | ||
children: <Widget>[ | ||
OBTextFormField( | ||
textCapitalization: | ||
TextCapitalization.sentences, | ||
size: OBTextFormFieldSize.large, | ||
autofocus: widget.autofocusNameTextField, | ||
controller: _nicknameController, | ||
decoration: InputDecoration( | ||
labelText: 'Nickname', | ||
hintText: 'e.g. Jane Doe'), | ||
validator: (String userInviteNickname) { | ||
if (!_formWasSubmitted) return null; | ||
return _validationService.validateUserProfileName(userInviteNickname); | ||
}), | ||
], | ||
)), | ||
], | ||
)), | ||
), | ||
)); | ||
} | ||
|
||
Widget _buildNavigationBar() { | ||
return OBThemedNavigationBar( | ||
leading: GestureDetector( | ||
child: const OBIcon(OBIcons.close), | ||
onTap: () { | ||
Navigator.pop(context); | ||
}, | ||
), | ||
title: _hasExistingUserInvite ? 'Edit invite' : 'Create invite', | ||
trailing: OBButton( | ||
isDisabled: !_formValid, | ||
isLoading: _requestInProgress, | ||
size: OBButtonSize.small, | ||
onPressed: _submitForm, | ||
child: Text('Next'), | ||
)); | ||
} | ||
|
||
bool _validateForm() { | ||
return _formKey.currentState.validate(); | ||
} | ||
|
||
bool _updateFormValid() { | ||
var formValid = _validateForm(); | ||
_setFormValid(formValid); | ||
return formValid; | ||
} | ||
|
||
void _submitForm() async { | ||
_formWasSubmitted = true; | ||
|
||
var formIsValid = _updateFormValid(); | ||
if (!formIsValid) return; | ||
_setRequestInProgress(true); | ||
try { | ||
|
||
_createUpdateOperation = CancelableOperation.fromFuture(_hasExistingUserInvite | ||
? _userService.updateUserInvite(userInvite: widget.userInvite, | ||
nickname: _nicknameController.text != widget.userInvite.nickname ? _nicknameController.text : null) | ||
: _userService.createUserInvite( | ||
nickname: _nicknameController.text)); | ||
|
||
UserInvite userInvite = await _createUpdateOperation.value; | ||
if (!_hasExistingUserInvite) { | ||
_navigateToShareInvite(userInvite); | ||
} else { | ||
Navigator.of(context).pop(userInvite); | ||
} | ||
} catch (error) { | ||
_onError(error); | ||
} finally { | ||
_setRequestInProgress(false); | ||
_createUpdateOperation = null; | ||
} | ||
} | ||
|
||
void _navigateToShareInvite(UserInvite userInvite) async { | ||
UserInvite sharedInvite = await _navigationService.navigateToShareInvite( | ||
context: context, | ||
userInvite: userInvite); | ||
Navigator.of(context).pop(userInvite); | ||
if (sharedInvite != null) { | ||
// Remove modal | ||
Navigator.of(context).pop(sharedInvite); | ||
} | ||
} | ||
|
||
void _onError(error) async { | ||
if (error is HttpieConnectionRefusedError) { | ||
_toastService.error( | ||
message: error.toHumanReadableMessage(), context: context); | ||
} else if (error is HttpieRequestError) { | ||
String errorMessage = await error.toHumanReadableMessage(); | ||
_toastService.error(message: errorMessage, context: context); | ||
} else { | ||
_toastService.error(message: 'Unknown error', context: context); | ||
throw error; | ||
} | ||
} | ||
|
||
void _setRequestInProgress(bool requestInProgress) { | ||
setState(() { | ||
_requestInProgress = requestInProgress; | ||
}); | ||
} | ||
|
||
void _setFormValid(bool formValid) { | ||
setState(() { | ||
_formValid = formValid; | ||
}); | ||
} | ||
|
||
} |
Oops, something went wrong.