Skip to content
This repository has been archived by the owner on Feb 4, 2022. It is now read-only.

Commit

Permalink
Merge pull request #260 from OpenbookOrg/feature/250-invite-friends
Browse files Browse the repository at this point in the history
Feature/250 invite friends
  • Loading branch information
uiboy authored Apr 24, 2019
2 parents 3e6fe52 + f6e2907 commit 665e1f2
Show file tree
Hide file tree
Showing 22 changed files with 1,798 additions and 4 deletions.
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ PODS:
- OneSignal (2.9.5)
- path_provider (0.0.1):
- Flutter
- share (0.5.2):
- Flutter
- sqflite (0.0.1):
- Flutter
- FMDB (~> 2.7.2)
Expand All @@ -46,6 +48,7 @@ DEPENDENCIES:
- OneSignal (< 3.0, >= 2.9.5)
- onesignal (from `.symlinks/plugins/onesignal/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- share (from `.symlinks/plugins/share/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
Expand Down Expand Up @@ -77,6 +80,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/onesignal/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
share:
:path: ".symlinks/plugins/share/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
uni_links:
Expand All @@ -99,6 +104,7 @@ SPEC CHECKSUMS:
OneSignal: ccdeb961882f8668305e5b694e2cb7cb325fc907
onesignal: c2122c20ffcb03d65445f3e0b49273c10f9c37a6
path_provider: 09407919825bfe3c2deae39453b7a5b44f467873
share: 222b5dcc8031238af9d7de91149df65bad1aef75
sqflite: d1612813fa7db7c667bed9f1d1b508deffc56999
TOCropViewController: 0a075f02c253e88095143bbac7b013fc6fba5090
uni_links: 5ee5240df5cbffc52d9e7f8017a576b6a6bc5141
Expand Down
4 changes: 4 additions & 0 deletions lib/models/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class User extends UpdatableModel<User> {
int followingCount;
int unreadNotificationsCount;
int postsCount;
int inviteCount;
bool isFollowing;
bool isConnected;
bool isFullyConnected;
Expand Down Expand Up @@ -77,6 +78,7 @@ class User extends UpdatableModel<User> {
this.followingCount,
this.unreadNotificationsCount,
this.postsCount,
this.inviteCount,
this.isFollowing,
this.isConnected,
this.isFullyConnected,
Expand Down Expand Up @@ -113,6 +115,7 @@ class User extends UpdatableModel<User> {
if (json.containsKey('unread_notifications_count'))
unreadNotificationsCount = json['unread_notifications_count'];
if (json.containsKey('posts_count')) postsCount = json['posts_count'];
if (json.containsKey('invite_count')) inviteCount = json['invite_count'];
if (json.containsKey('is_following')) isFollowing = json['is_following'];
if (json.containsKey('is_connected')) isConnected = json['is_connected'];
if (json.containsKey('connections_circle_id'))
Expand Down Expand Up @@ -297,6 +300,7 @@ class UserFactory extends UpdatableModelFactory<User> {
connectionsCircleId: json['connections_circle_id'],
followersCount: json['followers_count'],
postsCount: json['posts_count'],
inviteCount: json['invite_count'],
unreadNotificationsCount: json['unread_notifications_count'],
email: json['email'],
username: json['username'],
Expand Down
102 changes: 102 additions & 0 deletions lib/models/user_invite.dart
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 }
18 changes: 18 additions & 0 deletions lib/models/user_invites_list.dart
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 lib/pages/home/modals/user_invites/create_user_invite.dart
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;
});
}

}
Loading

0 comments on commit 665e1f2

Please sign in to comment.