[WIP] Add SubscriptionDetails, ProfileDetails and UsagesDetails in ProfileDialog

This commit is contained in:
Florian Rival
2017-12-29 02:00:04 +01:00
parent 4da6025b6f
commit 2afa54bcd2
13 changed files with 402 additions and 59 deletions

View File

@@ -18,6 +18,7 @@
"aws-sdk": "^2.100.0",
"axios": "^0.16.1",
"classnames": "2.2.5",
"date-fns": "^1.29.0",
"element-closest": "2.0.2",
"flat": "2.0.1",
"fontfaceobserver": "2.0.13",

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import { Column, Line } from '../UI/Grid';
const styles = {
orDivider: {
marginLeft: 15,
marginTop: 2,
},
};
export default props => (
<Column>
<Line>
<p>
You are not connected. Create a profile and connect to access to
GDevelop online services, like building your game for Android in one
click!
</p>
</Line>
<Line justifyContent="center" alignItems="center">
<RaisedButton label="Create my profile" onClick={props.onLogin} primary />
<span style={styles.orDivider}>or</span>
<FlatButton label="Login" onClick={props.onLogin} />
</Line>
</Column>
);

View File

@@ -0,0 +1,33 @@
// @flow
import * as React from 'react';
import Avatar from 'material-ui/Avatar';
import { Column, Line } from '../UI/Grid';
import { type Profile } from '../Utils/GDevelopServices/Authentification';
import PlaceholderLoader from '../UI/PlaceholderLoader';
const styles = {
title: {
fontSize: 25,
lineHeight: '40px',
marginLeft: 10,
},
};
type Props = {
profile: ?Profile,
};
export default ({ profile }: Props) =>
profile ? (
<Column>
<Line>
<Avatar src={profile.picture} />
<span style={styles.title}>You are connect as {profile.nickname}</span>
</Line>
<Line>
<p>With your account, you can access to GDevelop online services.</p>
</Line>
</Column>
) : (
<PlaceholderLoader />
);

View File

@@ -2,11 +2,23 @@
import React, { Component } from 'react';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import { Tabs, Tab } from 'material-ui/Tabs';
import Dialog from '../UI/Dialog';
import { Column, Line } from '../UI/Grid';
import Window from '../Utils/Window';
import Authentification, { type Profile } from './Authentification';
import { Column } from '../UI/Grid';
import Authentification, {
type Profile,
} from '../Utils/GDevelopServices/Authentification';
import CreateProfile from './CreateProfile';
import ProfileDetails from './ProfileDetails';
import {
getUserUsages,
type Usages,
type Subscription,
getUserSubscription,
} from '../Utils/GDevelopServices/Usage';
import EmptyMessage from '../UI/EmptyMessage';
import UsagesDetails from './UsagesDetails';
import SubscriptionDetails from './SubscriptionDetails';
type Props = {|
open: boolean,
@@ -15,14 +27,20 @@ type Props = {|
|};
type State = {|
currentTab: string,
authenticated: boolean,
profile: ?Profile,
usages: ?Usages,
subscription: ?Subscription,
|};
export default class PreferencesDialog extends Component<Props, State> {
export default class ProfileDialog extends Component<Props, State> {
state = {
currentTab: 'profile',
authenticated: false,
profile: null,
usages: null,
subscription: null,
};
componentWillMount() {
@@ -32,24 +50,36 @@ export default class PreferencesDialog extends Component<Props, State> {
? this.props.authentification.isAuthenticated()
: false,
});
}
if (this.props.open) {
this.fetchUserProfile();
}
}
componentWillReceiveProps(newProps: Props) {
if (!this.props.open && newProps.open) {
this.fetchUserProfile();
}
}
_onChangeTab = (newTab: string) =>
this.setState({
currentTab: newTab,
});
fetchUserProfile() {
console.log('Fetching');
const { authentification } = this.props;
if (!authentification) return;
authentification.getUserInfo((err, profile) => {
console.log('YOP', profile);
authentification.getUserInfo((err, profile: ?Profile) => {
if (err && err.unauthenticated) {
return this.setState({
authenticated: false,
profile: null,
usages: null,
});
} else if (err) {
} else if (err || !profile) {
console.log('Unable to fetch user profile', err);
return;
}
@@ -57,13 +87,23 @@ export default class PreferencesDialog extends Component<Props, State> {
this.setState({
profile,
});
Promise.all([
getUserUsages(authentification, profile.sub),
getUserSubscription(authentification, profile.sub),
]).then(([usages, subscription]) => {
this.setState({
usages,
subscription,
});
});
});
}
login = () => {
if (this.props.authentification)
this.props.authentification.login({
onHide: () => console.log('hidden'),
onHide: () => {},
onAuthenticated: arg => {
this.setState({
authenticated: true,
@@ -71,7 +111,7 @@ export default class PreferencesDialog extends Component<Props, State> {
this.fetchUserProfile();
},
onAuthorizationError: err => console.log('Authentification error', err),
onAuthorizationError: () => {},
});
};
@@ -84,38 +124,51 @@ export default class PreferencesDialog extends Component<Props, State> {
};
render() {
const { authenticated, profile } = this.state;
const { authenticated, profile, usages, subscription } = this.state;
const { open, onClose } = this.props;
const actions = [
<FlatButton label="Close" primary={false} onClick={onClose} />,
<FlatButton
label="Close"
key="close"
primary={false}
onClick={onClose}
/>,
];
return (
<Dialog
actions={actions}
secondaryActions={
authenticated && profile
? [<FlatButton label="Logout" key="logout" onClick={this.logout} />]
: []
}
onRequestClose={onClose}
open={open}
title="My profile"
noMargin
>
<Column>
<Line>
{authenticated && profile ? (
<div>
You're logged in as {profile.nickname}. Welcome!
<RaisedButton label="Logout" onClick={this.logout} />
</div>
<Tabs value={this.state.currentTab} onChange={this._onChangeTab}>
<Tab label="My Profile" value="profile">
{authenticated ? (
<Column noMargin>
<ProfileDetails profile={profile} />
<SubscriptionDetails subscription={subscription} />
</Column>
) : (
<div>
<RaisedButton
label="Create my profile"
onClick={this.login}
primary
/>
<FlatButton label="Login" onClick={this.login} />
</div>
<CreateProfile onLogin={this.login} />
)}
</Line>
</Column>
</Tab>
<Tab label="Online services usage" value="usage">
{authenticated ? (
<UsagesDetails usages={usages} />
) : (
<EmptyMessage>
Register to see the usage that you've made of the online
services
</EmptyMessage>
)}
</Tab>
</Tabs>
</Dialog>
);
}

View File

@@ -0,0 +1,42 @@
// @flow
import * as React from 'react';
import { Column, Line } from '../UI/Grid';
import { type Subscription } from '../Utils/GDevelopServices/Usage';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton';
type Props = {
subscription: ?Subscription,
};
export default ({ subscription }: Props) =>
subscription && subscription.planId ? (
<Column>
<Line>
<p>
You are subscribed to {subscription.planId}. Congratulations! You have
access to more online services, including building your game for
Android in one-click!
</p>
</Line>
<Line>
<RaisedButton label="Upgrade/change" disabled />
<FlatButton label="Cancel subscription" disabled />
</Line>
</Column>
) : subscription && !subscription.planId ? (
<Column>
<Line>
<p>
You don't have any subscription. Get one to access to all online
services, including building your game for Android in one-click!
</p>
</Line>
<Line>
<RaisedButton label="Choose a subscription" disabled />
</Line>
</Column>
) : (
<PlaceholderLoader />
);

View File

@@ -0,0 +1,49 @@
// @flow
import * as React from 'react';
import {
Table,
TableBody,
TableHeader,
TableHeaderColumn,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import { type Usages } from '../Utils/GDevelopServices/Usage';
import { Column, Line } from '../UI/Grid';
import EmptyMessage from '../UI/EmptyMessage';
import format from 'date-fns/format'
import PlaceholderLoader from '../UI/PlaceholderLoader';
type Props = { usages: ?Usages };
//TODO: Do a CircularProgress component that is centered?
export default ({ usages }: Props) => (
<Column noMargin>
<Line>
{!usages ? (
<PlaceholderLoader />
) : usages.length === 0 ? (
<EmptyMessage>
You don't have any usage of the online services for now
</EmptyMessage>
) : (
<Table selectable={false}>
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
<TableRow>
<TableHeaderColumn>Date</TableHeaderColumn>
<TableHeaderColumn>Type</TableHeaderColumn>
</TableRow>
</TableHeader>
<TableBody displayRowCheckbox={false}>
{usages.map(usage => (
<TableRow key={usage.id}>
<TableRowColumn>{format(usage.createdAt, 'YYYY-MM-DD HH:mm:ss')}</TableRowColumn>
<TableRowColumn>{usage.type}</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
)}
</Line>
</Column>
);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import CircularProgress from 'material-ui/CircularProgress';
const styles = {
containerStyle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
padding: 10,
},
};
export default props => (
<div style={{ ...styles.containerStyle, ...props.style }}>
<CircularProgress size={40} />
</div>
);

View File

@@ -1,4 +1,5 @@
// @flow
const isDev = process.env.NODE_ENV === 'development';
const gdevelopGamesPreviewRegion = 'eu-west-1';
const gdevelopGamesPreviewBucket = 'gd-games-preview';
@@ -41,5 +42,11 @@ export const Auth0Config = {
},
redirect: false,
},
}
},
};
export const GDevelopUsageApi = {
baseUrl: isDev
? 'https://tc1jkfw4ul.execute-api.us-east-1.amazonaws.com/dev'
: 'https://qe7jiozpz9.execute-api.us-east-1.amazonaws.com/live',
};

View File

@@ -1,32 +1,21 @@
// @flow
import Auth0Lock from 'auth0-lock';
const AUTH_CONFIG = {
domain: '4ian.eu.auth0.com',
clientId: 'vpsTe5CLJNp7K4nM1nQHzpkentyIZX5U',
};
import { Auth0Config } from './ApiConfigs';
export type Profile = {
sub: string, // This represents the userId
nickname: string,
picture: string,
email: string,
email_verified: boolean,
};
export default class Auth {
lock = new Auth0Lock(AUTH_CONFIG.clientId, AUTH_CONFIG.domain, {
autoclose: true,
theme: {
logo:
'https://raw.githubusercontent.com/4ian/GD/gh-pages/res/icon128linux.png',
primaryColor: '#4ab0e4',
},
auth: {
responseType: 'token id_token',
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
params: {
scope: 'openid profile email',
},
redirect: false,
},
});
export default class Authentification {
lock = new Auth0Lock(
Auth0Config.clientId,
Auth0Config.domain,
Auth0Config.lockOptions
);
constructor() {
this._handleAuthentication();
@@ -41,7 +30,6 @@ export default class Auth {
onAuthenticated: Function,
onAuthorizationError: Function,
}) {
// Call the show method to display the widget.
const noop = () => {};
this.lock.show();
this.lock.on('hide', onHide || noop);
@@ -66,7 +54,7 @@ export default class Auth {
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
}
}
};
getUserInfo = (cb: (any, ?Profile) => void) => {
if (!this.isAuthenticated()) cb({ unauthenticated: true });
@@ -80,13 +68,21 @@ export default class Auth {
console.log('Unable to fetch user info', err);
cb({ unknownError: true });
}
}
};
logout = () => {
// Clear access token and ID token from local storage
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
};
getAuthorizationHeader = () => {
try {
return 'Bearer ' + (localStorage.getItem('id_token') || '');
} catch (e) {
return ''
}
}
isAuthenticated = (): boolean => {
@@ -102,5 +98,5 @@ export default class Auth {
let expiresAt = JSON.parse(storedContent);
return new Date().getTime() < expiresAt;
}
};
}

View File

@@ -0,0 +1,84 @@
// @flow
import axios from 'axios';
import type Authentification from './Authentification';
import { GDevelopUsageApi } from './ApiConfigs';
export type Usage = {
id: string,
userId: string,
type: string,
createdAt: number,
};
export type Usages = Array<Usage>;
export type Subscription = {
userId: string,
planId: string,
createdAt: number,
updatedAt: number,
};
export type Limits = {
[string]: {
limitReached: boolean,
current: number,
max: number,
},
};
export const getSubscriptionPlans = () => ([
{
planId: 'gdevelop-indie',
},
{
planId: 'gdevelop-pro',
}
])
export const getUserUsages = (
authentification: Authentification,
userId: string
): Promise<Usages> => {
return axios
.get(`${GDevelopUsageApi.baseUrl}/usage`, {
params: {
userId,
},
headers: {
Authorization: authentification.getAuthorizationHeader(),
},
})
.then(response => response.data);
};
export const getUserSubscription = (
authentification: Authentification,
userId: string
): Promise<Subscription> => {
return axios
.get(`${GDevelopUsageApi.baseUrl}/subscription`, {
params: {
userId,
},
headers: {
Authorization: authentification.getAuthorizationHeader(),
},
})
.then(response => response.data);
};
export const getUserLimits = (
authentification: Authentification,
userId: string
): Promise<Limits> => {
return axios
.get(`${GDevelopUsageApi.baseUrl}/limits`, {
params: {
userId,
},
headers: {
Authorization: authentification.getAuthorizationHeader(),
},
})
.then(response => response.data);
};

View File

@@ -5,7 +5,7 @@ import MainFrame from './MainFrame';
import Window from './Utils/Window';
import ExportDialog from './Export/ExportDialog';
import CreateProjectDialog from './ProjectCreation/CreateProjectDialog';
import Authentification from './Profile/Authentification';
import Authentification from './Utils/GDevelopServices/Authentification';
import { sendProgramOpening } from './Utils/Analytics/EventSender';
import { installRaven } from './Utils/Analytics/Raven';
import { installFullstory } from './Utils/Analytics/Fullstory';

View File

@@ -14,6 +14,7 @@ import DragHandle from '../UI/DragHandle';
import LocalFolderPicker from '../UI/LocalFolderPicker';
import LocalExport from '../Export/LocalExport';
import LocalCordovaExport from '../Export/LocalCordovaExport';
import LocalOnlineCordovaExport from '../Export/LocalOnlineCordovaExport';
import LocalS3Export from '../Export/LocalS3Export';
import TextEditor from '../ObjectEditor/Editors/TextEditor';
import TiledSpriteEditor from '../ObjectEditor/Editors/TiledSpriteEditor';
@@ -49,6 +50,8 @@ import InstructionSelector from '../EventsSheet/InstructionEditor/InstructionOrE
import ParameterRenderingService from '../EventsSheet/InstructionEditor/ParameterRenderingService';
import {ErrorFallbackComponent} from '../UI/ErrorBoundary';
import { makeTestProject } from '../fixtures/TestProject';
import CreateProfile from '../Profile/CreateProfile';
import ProfileDetails from '../Profile/ProfileDetails';
const gd = global.gd;
const {
@@ -167,6 +170,11 @@ storiesOf('LocalCordovaExport', module)
.addDecorator(muiDecorator)
.add('default', () => <LocalCordovaExport project={project} />);
storiesOf('LocalOnlineCordovaExport', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => <LocalOnlineCordovaExport project={project} />);
storiesOf('LocalFolderPicker', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
@@ -462,3 +470,23 @@ storiesOf('ErrorBoundary', module)
.add('default', () => (
<ErrorFallbackComponent />
));
storiesOf('CreateProfile', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<CreateProfile onLogin={action('login')} />
));
storiesOf('ProfileDetails', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('profile', () => (
<ProfileDetails profile={{
nickname: 'Florian',
picture: '"https://s.gravatar.com/avatar/d6fc8df7ddfe938cc379c53bfb5645fc?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Ffl.png',
}} />
))
.add('loading', () => (
<ProfileDetails profile={null} />
));

View File

@@ -2524,6 +2524,10 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
date-fns@^1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"